mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-26 01:12:01 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
|
|
@ -1,15 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import resource
|
||||
from . import hr_employee
|
||||
from . import hr_employee_public
|
||||
from . import hr_department
|
||||
from . import hr_leave
|
||||
from . import hr_leave_allocation
|
||||
from . import hr_leave_type
|
||||
from . import hr_leave_accrual_plan_level
|
||||
from . import hr_leave_accrual_plan
|
||||
from . import hr_leave_stress_day
|
||||
from . import hr_leave_mandatory_day
|
||||
from . import hr_version
|
||||
from . import mail_activity_type
|
||||
from . import mail_message_subtype
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import calendar_event
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class CalendarEvent(models.Model):
|
||||
_inherit = 'calendar.event'
|
||||
|
||||
def _need_video_call(self):
|
||||
""" Determine if the event needs a video call or not depending
|
||||
on the model of the event.
|
||||
|
||||
This method, implemented and invoked in google_calendar, is necessary
|
||||
due to the absence of a bridge module between google_calendar and hr_holidays.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.res_model == 'hr.leave':
|
||||
return False
|
||||
return super()._need_video_call()
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
from datetime import datetime, timezone
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.osv import expression
|
||||
from odoo import fields, models
|
||||
from odoo.fields import Domain
|
||||
|
||||
import ast
|
||||
|
||||
|
||||
class Department(models.Model):
|
||||
|
||||
class HrDepartment(models.Model):
|
||||
_inherit = 'hr.department'
|
||||
|
||||
absence_of_today = fields.Integer(
|
||||
|
|
@ -23,26 +22,26 @@ class Department(models.Model):
|
|||
def _compute_leave_count(self):
|
||||
Requests = self.env['hr.leave']
|
||||
Allocations = self.env['hr.leave.allocation']
|
||||
today_date = datetime.datetime.utcnow().date()
|
||||
today_date = datetime.now(timezone.utc).date()
|
||||
today_start = fields.Datetime.to_string(today_date) # get the midnight of the current utc day
|
||||
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
|
||||
|
||||
leave_data = Requests._read_group(
|
||||
[('department_id', 'in', self.ids),
|
||||
('state', '=', 'confirm')],
|
||||
['department_id'], ['department_id'])
|
||||
['department_id'], ['__count'])
|
||||
allocation_data = Allocations._read_group(
|
||||
[('department_id', 'in', self.ids),
|
||||
('state', '=', 'confirm')],
|
||||
['department_id'], ['department_id'])
|
||||
['department_id'], ['__count'])
|
||||
absence_data = Requests._read_group(
|
||||
[('department_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']),
|
||||
[('department_id', 'in', self.ids), ('state', '=', 'validate'),
|
||||
('date_from', '<=', today_end), ('date_to', '>=', today_start)],
|
||||
['department_id'], ['department_id'])
|
||||
['department_id'], ['__count'])
|
||||
|
||||
res_leave = dict((data['department_id'][0], data['department_id_count']) for data in leave_data)
|
||||
res_allocation = dict((data['department_id'][0], data['department_id_count']) for data in allocation_data)
|
||||
res_absence = dict((data['department_id'][0], data['department_id_count']) for data in absence_data)
|
||||
res_leave = {department.id: count for department, count in leave_data}
|
||||
res_allocation = {department.id: count for department, count in allocation_data}
|
||||
res_absence = {department.id: count for department, count in absence_data}
|
||||
|
||||
for department in self:
|
||||
department.leave_to_approve_count = res_leave.get(department.id, 0)
|
||||
|
|
@ -63,8 +62,7 @@ class Department(models.Model):
|
|||
action['context'] = {
|
||||
**self._get_action_context(),
|
||||
'search_default_active_time_off': 3,
|
||||
'hide_employee_name': 1,
|
||||
'holiday_status_name_get': False
|
||||
'hide_employee_name': 1
|
||||
}
|
||||
return action
|
||||
|
||||
|
|
@ -72,5 +70,5 @@ class Department(models.Model):
|
|||
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_approve_department")
|
||||
action['context'] = self._get_action_context()
|
||||
action['context']['search_default_second_approval'] = 3
|
||||
action['domain'] = expression.AND([ast.literal_eval(action['domain']), [('state', '=', 'confirm')]])
|
||||
action['domain'] = Domain.AND([ast.literal_eval(action['domain']), [('state', '=', 'confirm')]])
|
||||
return action
|
||||
|
|
|
|||
|
|
@ -1,87 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
from ast import literal_eval
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from collections import defaultdict
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.addons.resource.models.resource import HOURS_PER_DAY
|
||||
import pytz
|
||||
|
||||
class HrEmployeeBase(models.AbstractModel):
|
||||
_inherit = "hr.employee.base"
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.resource.models.utils import HOURS_PER_DAY
|
||||
from odoo.tools.float_utils import float_round
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
leave_manager_id = fields.Many2one(
|
||||
'res.users', string='Time Off',
|
||||
'res.users', string='Time Off Approver',
|
||||
compute='_compute_leave_manager', store=True, readonly=False,
|
||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
||||
help='Select the user responsible for approving "Time Off" of this employee.\n'
|
||||
'If empty, the approval is done by an Administrator or Approver (determined in settings/users).')
|
||||
remaining_leaves = fields.Float(
|
||||
compute='_compute_remaining_leaves', string='Remaining Paid Time Off',
|
||||
help='Total number of paid time off allocated to this employee, change this value to create allocation/time off request. '
|
||||
'Total based on all the time off types without overriding limit.')
|
||||
current_leave_id = fields.Many2one('hr.leave.type', compute='_compute_current_leave', string="Current Time Off Type",
|
||||
groups="hr.group_hr_user")
|
||||
current_leave_state = fields.Selection(compute='_compute_leave_status', string="Current Time Off Status",
|
||||
selection=[
|
||||
('draft', 'New'),
|
||||
('confirm', 'Waiting Approval'),
|
||||
('refuse', 'Refused'),
|
||||
('validate1', 'Waiting Second Approval'),
|
||||
('validate', 'Approved'),
|
||||
('cancel', 'Cancelled')
|
||||
])
|
||||
leave_date_from = fields.Date('From Date', compute='_compute_leave_status')
|
||||
('cancel', 'Cancelled'),
|
||||
], groups="hr.group_hr_user")
|
||||
leave_date_from = fields.Date('From Date', compute='_compute_leave_status', groups="hr.group_hr_user")
|
||||
leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
|
||||
leaves_count = fields.Float('Number of Time Off', compute='_compute_remaining_leaves')
|
||||
allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count')
|
||||
allocations_count = fields.Integer('Total number of allocations', compute="_compute_allocation_count")
|
||||
allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count',
|
||||
groups="hr.group_hr_user")
|
||||
allocations_count = fields.Integer('Total number of allocations', compute="_compute_allocation_count",
|
||||
groups="hr.group_hr_user")
|
||||
show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves')
|
||||
is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee')
|
||||
allocation_display = fields.Char(compute='_compute_allocation_count')
|
||||
allocation_display = fields.Char(compute='_compute_allocation_remaining_display')
|
||||
allocation_remaining_display = fields.Char(compute='_compute_allocation_remaining_display')
|
||||
hr_icon_display = fields.Selection(selection_add=[('presence_holiday_absent', 'On leave'),
|
||||
('presence_holiday_present', 'Present but on leave')])
|
||||
hr_icon_display = fields.Selection(selection_add=[
|
||||
('presence_holiday_absent', 'On leave'),
|
||||
('presence_holiday_present', 'Present but on leave')])
|
||||
|
||||
def _get_remaining_leaves(self):
|
||||
""" Helper to compute the remaining leaves for the current employees
|
||||
:returns dict where the key is the employee id, and the value is the remain leaves
|
||||
"""
|
||||
self._cr.execute("""
|
||||
SELECT
|
||||
sum(h.number_of_days) AS days,
|
||||
h.employee_id
|
||||
FROM
|
||||
(
|
||||
SELECT holiday_status_id, number_of_days,
|
||||
state, employee_id
|
||||
FROM hr_leave_allocation
|
||||
UNION ALL
|
||||
SELECT holiday_status_id, (number_of_days * -1) as number_of_days,
|
||||
state, employee_id
|
||||
FROM hr_leave
|
||||
) h
|
||||
join hr_leave_type s ON (s.id=h.holiday_status_id)
|
||||
WHERE
|
||||
s.active = true AND h.state='validate' AND
|
||||
s.requires_allocation='yes' AND
|
||||
h.employee_id in %s
|
||||
GROUP BY h.employee_id""", (tuple(self.ids),))
|
||||
return dict((row['employee_id'], row['days']) for row in self._cr.dictfetchall())
|
||||
def _compute_current_leave(self):
|
||||
self.current_leave_id = False
|
||||
|
||||
def _compute_remaining_leaves(self):
|
||||
remaining = {}
|
||||
if self.ids:
|
||||
remaining = self._get_remaining_leaves()
|
||||
for employee in self:
|
||||
value = float_round(remaining.get(employee.id, 0.0), precision_digits=2)
|
||||
employee.leaves_count = value
|
||||
employee.remaining_leaves = value
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', 'in', self.ids),
|
||||
('date_from', '<=', fields.Datetime.now()),
|
||||
('date_to', '>=', fields.Datetime.now()),
|
||||
('state', '=', 'validate'),
|
||||
])
|
||||
for holiday in holidays:
|
||||
employee = self.filtered(lambda e: e.id == holiday.employee_id.id)
|
||||
employee.current_leave_id = holiday.holiday_status_id.id
|
||||
|
||||
def _compute_presence_state(self):
|
||||
super()._compute_presence_state()
|
||||
employees = self.filtered(lambda employee: employee.hr_presence_state != 'present' and employee.is_absent)
|
||||
employees.update({'hr_presence_state': 'absent'})
|
||||
|
||||
def _compute_allocation_count(self):
|
||||
# Don't get allocations that are expired
|
||||
current_date = datetime.date.today()
|
||||
current_date = date.today()
|
||||
data = self.env['hr.leave.allocation']._read_group([
|
||||
('employee_id', 'in', self.ids),
|
||||
('holiday_status_id.active', '=', True),
|
||||
|
|
@ -91,45 +75,63 @@ class HrEmployeeBase(models.AbstractModel):
|
|||
'|',
|
||||
('date_to', '=', False),
|
||||
('date_to', '>=', current_date),
|
||||
], ['number_of_days:sum', 'employee_id'], ['employee_id'])
|
||||
rg_results = dict((d['employee_id'][0], {"employee_id_count": d['employee_id_count'], "number_of_days": d['number_of_days']}) for d in data)
|
||||
], ['employee_id'], ['__count', 'number_of_days:sum'])
|
||||
rg_results = {employee.id: (count, days) for employee, count, days in data}
|
||||
for employee in self:
|
||||
result = rg_results.get(employee.id)
|
||||
employee.allocation_count = float_round(result['number_of_days'], precision_digits=2) if result else 0.0
|
||||
employee.allocation_display = "%g" % employee.allocation_count
|
||||
employee.allocations_count = result['employee_id_count'] if result else 0.0
|
||||
count, days = rg_results.get(employee.id, (0, 0))
|
||||
employee.allocation_count = float_round(days, precision_digits=2)
|
||||
employee.allocations_count = count
|
||||
|
||||
def _compute_allocation_remaining_display(self):
|
||||
current_date = date.today()
|
||||
allocations = self.env['hr.leave.allocation'].search([('employee_id', 'in', self.ids)])
|
||||
leaves_taken = allocations.holiday_status_id._get_employees_days_per_allocation(self.ids)
|
||||
leaves_taken = self._get_consumed_leaves(allocations.holiday_status_id)[0]
|
||||
for employee in self:
|
||||
employee_remaining_leaves = 0
|
||||
for leave_type in leaves_taken[employee.id]:
|
||||
if leave_type.requires_allocation == 'no':
|
||||
employee_max_leaves = 0
|
||||
for leave_type in leaves_taken[employee]:
|
||||
if leave_type.requires_allocation == 'no' or leave_type.hide_on_dashboard or not leave_type.active:
|
||||
continue
|
||||
for allocation in leaves_taken[employee.id][leave_type]:
|
||||
if allocation:
|
||||
virtual_remaining_leaves = leaves_taken[employee.id][leave_type][allocation]['virtual_remaining_leaves']
|
||||
for allocation in leaves_taken[employee][leave_type]:
|
||||
if allocation and allocation.date_from <= current_date\
|
||||
and (not allocation.date_to or allocation.date_to >= current_date):
|
||||
virtual_remaining_leaves = leaves_taken[employee][leave_type][allocation]['virtual_remaining_leaves']
|
||||
employee_remaining_leaves += virtual_remaining_leaves\
|
||||
if leave_type.request_unit in ['day', 'half_day']\
|
||||
else virtual_remaining_leaves / (employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
|
||||
employee_max_leaves += allocation.number_of_days
|
||||
employee.allocation_remaining_display = "%g" % float_round(employee_remaining_leaves, precision_digits=2)
|
||||
|
||||
def _compute_presence_state(self):
|
||||
super()._compute_presence_state()
|
||||
employees = self.filtered(lambda employee: employee.hr_presence_state != 'present' and employee.is_absent)
|
||||
employees.update({'hr_presence_state': 'absent'})
|
||||
employee.allocation_display = "%g" % float_round(employee_max_leaves, precision_digits=2)
|
||||
|
||||
def _compute_presence_icon(self):
|
||||
super()._compute_presence_icon()
|
||||
employees_absent = self.filtered(lambda employee:
|
||||
employee.hr_presence_state != 'present'
|
||||
and employee.is_absent)
|
||||
employees_absent.update({'hr_icon_display': 'presence_holiday_absent'})
|
||||
employees_present = self.filtered(lambda employee:
|
||||
employee.hr_presence_state == 'present'
|
||||
and employee.is_absent)
|
||||
employees_present.update({'hr_icon_display': 'presence_holiday_present'})
|
||||
employees_absent = self.filtered(
|
||||
lambda employee: employee.hr_presence_state != 'present' and employee.is_absent)
|
||||
employees_absent.update({'hr_icon_display': 'presence_holiday_absent', 'show_hr_icon_display': True})
|
||||
employees_present = self.filtered(
|
||||
lambda employee: employee.hr_presence_state == 'present' and employee.is_absent)
|
||||
employees_present.update({'hr_icon_display': 'presence_holiday_present', 'show_hr_icon_display': True})
|
||||
|
||||
def _get_first_working_interval(self, dt):
|
||||
# find the first working interval after a given date
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
lookahead_days = [7, 30, 90, 180, 365, 730]
|
||||
work_intervals = None
|
||||
for lookahead_day in lookahead_days:
|
||||
periods = self._get_calendar_periods(dt, dt + timedelta(days=lookahead_day))
|
||||
if not periods:
|
||||
calendar = self.resource_calendar_id or self.company_id.resource_calendar_id
|
||||
work_intervals = calendar._work_intervals_batch(
|
||||
dt, dt + timedelta(days=lookahead_day), resources=self.resource_id)
|
||||
else:
|
||||
for period in periods[self]:
|
||||
start, end, calendar = period
|
||||
calendar = calendar or self.company_id.resource_calendar_id
|
||||
work_intervals = calendar._work_intervals_batch(
|
||||
start, end, resources=self.resource_id)
|
||||
if work_intervals.get(self.resource_id.id) and work_intervals[self.resource_id.id]._items:
|
||||
# return start time of the earliest interval
|
||||
return work_intervals[self.resource_id.id]._items[0][0]
|
||||
|
||||
def _compute_leave_status(self):
|
||||
# Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
|
||||
|
|
@ -137,20 +139,22 @@ class HrEmployeeBase(models.AbstractModel):
|
|||
('employee_id', 'in', self.ids),
|
||||
('date_from', '<=', fields.Datetime.now()),
|
||||
('date_to', '>=', fields.Datetime.now()),
|
||||
('holiday_status_id.time_type', '=', 'leave'),
|
||||
('state', '=', 'validate'),
|
||||
])
|
||||
leave_data = {}
|
||||
for holiday in holidays:
|
||||
leave_data[holiday.employee_id.id] = {}
|
||||
leave_data[holiday.employee_id.id]['leave_date_from'] = holiday.date_from.date()
|
||||
leave_data[holiday.employee_id.id]['leave_date_to'] = holiday.date_to.date()
|
||||
back_on = holiday.employee_id._get_first_working_interval(holiday.date_to)
|
||||
leave_data[holiday.employee_id.id]['leave_date_to'] = back_on.date() if back_on else None
|
||||
leave_data[holiday.employee_id.id]['current_leave_state'] = holiday.state
|
||||
|
||||
for employee in self:
|
||||
employee.leave_date_from = leave_data.get(employee.id, {}).get('leave_date_from')
|
||||
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
|
||||
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
|
||||
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') in ['validate']
|
||||
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id).get('current_leave_state') == 'validate'
|
||||
|
||||
@api.depends('parent_id')
|
||||
def _compute_leave_manager(self):
|
||||
|
|
@ -163,7 +167,7 @@ class HrEmployeeBase(models.AbstractModel):
|
|||
employee.leave_manager_id = False
|
||||
|
||||
def _compute_show_leaves(self):
|
||||
show_leaves = self.env['res.users'].has_group('hr_holidays.group_hr_holidays_user')
|
||||
show_leaves = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
|
||||
for employee in self:
|
||||
if show_leaves or employee.user_id == self.env.user:
|
||||
employee.show_leaves = True
|
||||
|
|
@ -171,21 +175,19 @@ class HrEmployeeBase(models.AbstractModel):
|
|||
employee.show_leaves = False
|
||||
|
||||
def _search_absent_employee(self, operator, value):
|
||||
if operator not in ('=', '!=') or not isinstance(value, bool):
|
||||
raise UserError(_('Operation not supported'))
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
# This search is only used for the 'Absent Today' filter however
|
||||
# this only returns employees that are absent right now.
|
||||
today_date = datetime.datetime.utcnow().date()
|
||||
today_start = fields.Datetime.to_string(today_date)
|
||||
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
|
||||
today_start = date.today()
|
||||
today_end = today_start + timedelta(1)
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', '!=', False),
|
||||
('state', '=', 'validate'),
|
||||
('date_from', '<=', today_end),
|
||||
('date_from', '<', today_end),
|
||||
('date_to', '>=', today_start),
|
||||
])
|
||||
operator = ['in', 'not in'][(operator == '=') != value]
|
||||
return [('id', operator, holidays.mapped('employee_id').ids)]
|
||||
return [('id', 'in', holidays.employee_id.ids)]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
|
@ -200,10 +202,15 @@ class HrEmployeeBase(models.AbstractModel):
|
|||
if approver_group and vals.get('leave_manager_id'):
|
||||
group_updates.append((4, vals['leave_manager_id']))
|
||||
if group_updates:
|
||||
approver_group.sudo().write({'users': group_updates})
|
||||
approver_group.sudo().write({'user_ids': group_updates})
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
def write(self, vals):
|
||||
values = vals
|
||||
# Prevent the resource calendar of leaves to be updated by a write to
|
||||
# employee. When this module is enabled the resource calendar of
|
||||
# leaves are determined by those of the contracts.
|
||||
self = self.with_context(no_leave_resource_calendar_update=True) # noqa: PLW0642
|
||||
if 'parent_id' in values:
|
||||
manager = self.env['hr.employee'].browse(values['parent_id']).user_id
|
||||
if manager:
|
||||
|
|
@ -218,44 +225,51 @@ class HrEmployeeBase(models.AbstractModel):
|
|||
old_managers -= leave_manager
|
||||
approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
|
||||
if approver_group and not leave_manager.has_group('hr_holidays.group_hr_holidays_responsible'):
|
||||
leave_manager.sudo().write({'groups_id': [(4, approver_group.id)]})
|
||||
leave_manager.sudo().write({'group_ids': [(4, approver_group.id)]})
|
||||
|
||||
res = super(HrEmployeeBase, self).write(values)
|
||||
res = super().write(values)
|
||||
# remove users from the Responsible group if they are no longer leave managers
|
||||
old_managers.sudo()._clean_leave_responsible_users()
|
||||
|
||||
# Change the resource calendar of the employee's leaves in the future
|
||||
# Other modules can disable this behavior by setting the context key
|
||||
# 'no_leave_resource_calendar_update'
|
||||
if 'resource_calendar_id' in values and not self.env.context.get('no_leave_resource_calendar_update'):
|
||||
try:
|
||||
leaves = self.env['hr.leave'].search([
|
||||
('employee_id', 'in', self.ids),
|
||||
('resource_calendar_id', '!=', int(values['resource_calendar_id'])),
|
||||
('date_from', '>', fields.Datetime.now())])
|
||||
leaves.write({'resource_calendar_id': values['resource_calendar_id']})
|
||||
non_hourly_leaves = leaves.filtered(lambda l: not l.request_unit_hours)
|
||||
non_hourly_leaves.with_context(leave_skip_date_check=True, leave_skip_state_check=True)._compute_date_from_to()
|
||||
non_hourly_leaves.filtered(lambda l: l.state == 'validate')._validate_leave_request()
|
||||
except ValidationError:
|
||||
raise ValidationError(_("Changing this working schedule results in the affected employee(s) not having enough "
|
||||
"leaves allocated to accomodate for their leaves already taken in the future. Please "
|
||||
"review this employee's leaves and adjust their allocation accordingly."))
|
||||
|
||||
if 'parent_id' in values or 'department_id' in values:
|
||||
today_date = fields.Datetime.now()
|
||||
hr_vals = {}
|
||||
if values.get('parent_id') is not None:
|
||||
hr_vals['manager_id'] = values['parent_id']
|
||||
if values.get('department_id') is not None:
|
||||
hr_vals['department_id'] = values['department_id']
|
||||
holidays = self.env['hr.leave'].sudo().search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
'|',
|
||||
('state', '=', 'confirm'),
|
||||
('date_from', '>', today_date),
|
||||
('employee_id', 'in', self.ids),
|
||||
])
|
||||
holidays.write(hr_vals)
|
||||
allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
|
||||
if values.get('parent_id') is not None:
|
||||
hr_vals['manager_id'] = values['parent_id']
|
||||
allocations = self.env['hr.leave.allocation'].sudo().search([
|
||||
('state', '=', 'confirm'),
|
||||
('employee_id', 'in', self.ids),
|
||||
])
|
||||
allocations.write(hr_vals)
|
||||
return res
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
current_leave_id = fields.Many2one('hr.leave.type', compute='_compute_current_leave', string="Current Time Off Type",
|
||||
groups="hr.group_hr_user")
|
||||
|
||||
def _compute_current_leave(self):
|
||||
self.current_leave_id = False
|
||||
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', 'in', self.ids),
|
||||
('date_from', '<=', fields.Datetime.now()),
|
||||
('date_to', '>=', fields.Datetime.now()),
|
||||
('state', '=', 'validate'),
|
||||
])
|
||||
for holiday in holidays:
|
||||
employee = self.filtered(lambda e: e.id == holiday.employee_id.id)
|
||||
employee.current_leave_id = holiday.holiday_status_id.id
|
||||
|
||||
def _get_user_m2o_to_empty_on_archived_employees(self):
|
||||
return super()._get_user_m2o_to_empty_on_archived_employees() + ['leave_manager_id']
|
||||
|
||||
|
|
@ -271,31 +285,23 @@ class HrEmployee(models.Model):
|
|||
},
|
||||
}
|
||||
|
||||
def _get_contextual_employee(self):
|
||||
if self.env.context.get('employee_id'):
|
||||
return self.browse(self.env.context['employee_id'])
|
||||
return self.env.user.employee_id
|
||||
|
||||
def _is_leave_user(self):
|
||||
return self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user')
|
||||
|
||||
def get_stress_days(self, start_date, end_date):
|
||||
def get_mandatory_days(self, start_date, end_date):
|
||||
all_days = {}
|
||||
|
||||
self = self or self.env.user.employee_id
|
||||
|
||||
stress_days = self._get_stress_days(start_date, end_date)
|
||||
for stress_day in stress_days:
|
||||
num_days = (stress_day.end_date - stress_day.start_date).days
|
||||
mandatory_days = self._get_mandatory_days(start_date, end_date)
|
||||
for mandatory_day in mandatory_days:
|
||||
num_days = (mandatory_day.end_date - mandatory_day.start_date).days
|
||||
for d in range(num_days + 1):
|
||||
all_days[str(stress_day.start_date + relativedelta(days=d))] = stress_day.color
|
||||
all_days[str(mandatory_day.start_date + relativedelta(days=d))] = mandatory_day.color
|
||||
|
||||
return all_days
|
||||
|
||||
@api.model
|
||||
def get_special_days_data(self, date_start, date_end):
|
||||
return {
|
||||
'stressDays': self.get_stress_days_data(date_start, date_end),
|
||||
'mandatoryDays': self.get_mandatory_days_data(date_start, date_end),
|
||||
'bankHolidays': self.get_public_holidays_data(date_start, date_end),
|
||||
}
|
||||
|
||||
|
|
@ -307,14 +313,30 @@ class HrEmployee(models.Model):
|
|||
return list(map(lambda bh: {
|
||||
'id': -bh.id,
|
||||
'colorIndex': 0,
|
||||
'end': datetime.datetime.combine(bh.date_to.astimezone(employee_tz), datetime.datetime.max.time()).isoformat(),
|
||||
'end': datetime.combine(bh.date_to.astimezone(employee_tz), datetime.max.time()).isoformat(),
|
||||
'endType': "datetime",
|
||||
'isAllDay': True,
|
||||
'start': datetime.datetime.combine(bh.date_from.astimezone(employee_tz), datetime.datetime.min.time()).isoformat(),
|
||||
'start': datetime.combine(bh.date_from.astimezone(employee_tz), datetime.min.time()).isoformat(),
|
||||
'startType': "datetime",
|
||||
'title': bh.name,
|
||||
}, public_holidays))
|
||||
|
||||
@api.model
|
||||
def get_time_off_dashboard_data(self, target_date=None):
|
||||
dashboard_data = {}
|
||||
dashboard_data['has_accrual_allocation'] = self.env['hr.leave.type'].has_accrual_allocation()
|
||||
dashboard_data['allocation_data'] = self.env['hr.leave.type'].get_allocation_data_request(target_date, False)
|
||||
dashboard_data['allocation_request_amount'] = self.get_allocation_requests_amount()
|
||||
return dashboard_data
|
||||
|
||||
@api.model
|
||||
def get_allocation_requests_amount(self):
|
||||
employee = self._get_contextual_employee()
|
||||
return self.env['hr.leave.allocation'].search_count([
|
||||
('employee_id', '=', employee.id),
|
||||
('state', '=', 'confirm'),
|
||||
])
|
||||
|
||||
def _get_public_holidays(self, date_start, date_end):
|
||||
domain = [
|
||||
('resource_id', '=', False),
|
||||
|
|
@ -329,37 +351,272 @@ class HrEmployee(models.Model):
|
|||
return self.env['resource.calendar.leaves'].search(domain)
|
||||
|
||||
@api.model
|
||||
def get_stress_days_data(self, date_start, date_end):
|
||||
self = self._get_contextual_employee()
|
||||
stress_days = self._get_stress_days(date_start, date_end).sorted('start_date')
|
||||
return list(map(lambda sd: {
|
||||
def get_mandatory_days_data(self, date_start, date_end):
|
||||
self_with_context = self._get_contextual_employee()
|
||||
if isinstance(date_start, str):
|
||||
date_start = datetime.fromisoformat(date_start).replace(tzinfo=None)
|
||||
elif isinstance(date_start, datetime):
|
||||
date_start = date_start.replace(tzinfo=None)
|
||||
|
||||
if isinstance(date_end, str):
|
||||
date_end = datetime.fromisoformat(date_end).replace(tzinfo=None)
|
||||
elif isinstance(date_end, datetime):
|
||||
date_end = date_end.replace(tzinfo=None)
|
||||
|
||||
mandatory_days = self_with_context._get_mandatory_days(date_start, date_end).sorted('start_date')
|
||||
return [{
|
||||
'id': -sd.id,
|
||||
'colorIndex': sd.color,
|
||||
'end': datetime.datetime.combine(sd.end_date, datetime.datetime.max.time()).isoformat(),
|
||||
'end': datetime.combine(sd.end_date, datetime.max.time()).isoformat(),
|
||||
'endType': "datetime",
|
||||
'isAllDay': True,
|
||||
'start': datetime.datetime.combine(sd.start_date, datetime.datetime.min.time()).isoformat(),
|
||||
'start': datetime.combine(sd.start_date, datetime.min.time()).isoformat(),
|
||||
'startType': "datetime",
|
||||
'title': sd.name,
|
||||
}, stress_days))
|
||||
} for sd in mandatory_days]
|
||||
|
||||
def _get_stress_days(self, start_date, end_date):
|
||||
def _get_mandatory_days(self, start_date, end_date):
|
||||
domain = [
|
||||
('start_date', '<=', end_date),
|
||||
('end_date', '>=', start_date),
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
'|',
|
||||
('resource_calendar_id', '=', False),
|
||||
('resource_calendar_id', '=', self.resource_calendar_id.id),
|
||||
('resource_calendar_id', 'in', self.resource_calendar_id.ids),
|
||||
]
|
||||
|
||||
if self.job_id:
|
||||
domain += [
|
||||
('job_ids', 'in', [False] + self.job_id.ids),
|
||||
]
|
||||
if self.department_id:
|
||||
department_ids = self.department_id.ids
|
||||
domain += [
|
||||
'|',
|
||||
('department_ids', '=', False),
|
||||
('department_ids', 'parent_of', self.department_id.id),
|
||||
('department_ids', 'parent_of', department_ids),
|
||||
]
|
||||
else:
|
||||
domain += [('department_ids', '=', False)]
|
||||
|
||||
return self.env['hr.leave.stress.day'].search(domain)
|
||||
return self.env['hr.leave.mandatory.day'].search(domain)
|
||||
|
||||
@api.model
|
||||
def _get_contextual_employee(self):
|
||||
ctx = self.env.context
|
||||
if self.env.context.get('employee_id') is not None:
|
||||
return self.browse(ctx.get('employee_id'))
|
||||
if self.env.context.get('default_employee_id') is not None:
|
||||
return self.browse(ctx.get('default_employee_id'))
|
||||
return self.env.user.employee_id
|
||||
|
||||
def _get_consumed_leaves(self, leave_types, target_date=False, ignore_future=False):
|
||||
employees = self or self._get_contextual_employee()
|
||||
leaves_domain = [
|
||||
('holiday_status_id', 'in', leave_types.ids),
|
||||
('employee_id', 'in', employees.ids),
|
||||
('state', 'in', ['confirm', 'validate1', 'validate']),
|
||||
]
|
||||
if self.env.context.get('ignored_leave_ids'):
|
||||
leaves_domain.append(('id', 'not in', self.env.context.get('ignored_leave_ids')))
|
||||
|
||||
if not target_date:
|
||||
target_date = fields.Date.today()
|
||||
if ignore_future:
|
||||
leaves_domain.append(('date_from', '<=', target_date))
|
||||
leaves = self.env['hr.leave'].search(leaves_domain)
|
||||
leaves_per_employee_type = defaultdict(lambda: defaultdict(lambda: self.env['hr.leave']))
|
||||
for leave in leaves:
|
||||
leaves_per_employee_type[leave.employee_id][leave.holiday_status_id] |= leave
|
||||
|
||||
allocations = self.env['hr.leave.allocation'].with_context(active_test=False).search([
|
||||
('employee_id', 'in', employees.ids),
|
||||
('holiday_status_id', 'in', leave_types.ids),
|
||||
('state', '=', 'validate'),
|
||||
])
|
||||
allocations_per_employee_type = defaultdict(lambda: defaultdict(lambda: self.env['hr.leave.allocation']))
|
||||
for allocation in allocations:
|
||||
allocations_per_employee_type[allocation.employee_id][allocation.holiday_status_id] |= allocation
|
||||
|
||||
# _get_consumed_leaves returns a tuple of two dictionnaries.
|
||||
# 1) The first is a dictionary to map the number of days/hours of leaves taken per allocation
|
||||
# The structure is the following:
|
||||
# - KEYS:
|
||||
# allocation_leaves_consumed
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# |--allocation
|
||||
# |--virtual_leaves_taken
|
||||
# |--leaves_taken
|
||||
# |--virtual_remaining_leaves
|
||||
# |--remaining_leaves
|
||||
# |--max_leaves
|
||||
# |--accrual_bonus
|
||||
# - VALUES:
|
||||
# Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves
|
||||
# for each allocation.
|
||||
# leaves_taken and remaining_leaves only take into account validated leaves, while the "virtual" equivalent are
|
||||
# also based on leaves in "confirm" or "validate1" state.
|
||||
# Accrual bonus gives the amount of additional leaves that will have been granted at the given
|
||||
# target_date in comparison to today.
|
||||
# The unit is in hour or days depending on the leave type request unit
|
||||
# 2) The second is a dictionary mapping the remaining days per employee and per leave type that are either
|
||||
# not taken into account by the allocations, mainly because accruals don't take future leaves into account.
|
||||
# This is used to warn the user if the leaves they takes bring them above their available limit.
|
||||
# - KEYS:
|
||||
# allocation_leaves_consumed
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# |--to_recheck_leaves
|
||||
# |--excess_days
|
||||
# |--exceeding_duration
|
||||
# - VALUES:
|
||||
# "to_recheck_leaves" stores every leave that is not yet taken into account by the "allocation_leaves_consumed" dictionary.
|
||||
# "excess_days" represents the excess amount that somehow isn't taken into account by the first dictionary.
|
||||
# "exceeding_duration" sum up the to_recheck_leaves duration and compares it to the maximum allocated for that time period.
|
||||
allocations_leaves_consumed = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))))
|
||||
|
||||
to_recheck_leaves_per_leave_type = defaultdict(lambda:
|
||||
defaultdict(lambda: {
|
||||
'excess_days': defaultdict(lambda: {
|
||||
'amount': 0,
|
||||
'is_virtual': True,
|
||||
}),
|
||||
'exceeding_duration': 0,
|
||||
'to_recheck_leaves': self.env['hr.leave']
|
||||
})
|
||||
)
|
||||
for allocation in allocations:
|
||||
allocation_data = allocations_leaves_consumed[allocation.employee_id][allocation.holiday_status_id][allocation]
|
||||
future_leaves = 0
|
||||
if allocation.allocation_type == 'accrual':
|
||||
future_leaves = allocation._get_future_leaves_on(target_date)
|
||||
max_leaves = allocation.number_of_hours_display\
|
||||
if allocation.holiday_status_id.request_unit in ['hour']\
|
||||
else allocation.number_of_days_display
|
||||
max_leaves += future_leaves
|
||||
allocation_data.update({
|
||||
'max_leaves': max_leaves,
|
||||
'accrual_bonus': future_leaves,
|
||||
'virtual_remaining_leaves': max_leaves,
|
||||
'remaining_leaves': max_leaves,
|
||||
'leaves_taken': 0,
|
||||
'virtual_leaves_taken': 0,
|
||||
})
|
||||
|
||||
for employee in employees:
|
||||
for leave_type in leave_types:
|
||||
allocations_with_date_to = self.env['hr.leave.allocation']
|
||||
allocations_without_date_to = self.env['hr.leave.allocation']
|
||||
for leave_allocation in allocations_per_employee_type[employee][leave_type]:
|
||||
if leave_allocation.date_to:
|
||||
allocations_with_date_to |= leave_allocation
|
||||
else:
|
||||
allocations_without_date_to |= leave_allocation
|
||||
sorted_leave_allocations = allocations_with_date_to.sorted(key='date_to') + allocations_without_date_to
|
||||
|
||||
if leave_type.request_unit in ['day', 'half_day']:
|
||||
leave_duration_field = 'number_of_days'
|
||||
leave_unit = 'days'
|
||||
else:
|
||||
leave_duration_field = 'number_of_hours'
|
||||
leave_unit = 'hours'
|
||||
|
||||
leave_type_data = allocations_leaves_consumed[employee][leave_type]
|
||||
for leave in leaves_per_employee_type[employee][leave_type].sorted('date_from'):
|
||||
leave_duration = leave[leave_duration_field]
|
||||
skip_excess = False
|
||||
|
||||
if leave.date_from.date() > target_date and sorted_leave_allocations.filtered(lambda a:
|
||||
a.allocation_type == 'accrual' and
|
||||
(not a.date_to or a.date_to >= target_date) and
|
||||
a.date_from <= leave.date_to.date()
|
||||
):
|
||||
to_recheck_leaves_per_leave_type[employee][leave_type]['to_recheck_leaves'] |= leave
|
||||
skip_excess = True
|
||||
continue
|
||||
|
||||
if leave_type.requires_allocation:
|
||||
for allocation in sorted_leave_allocations:
|
||||
# We don't want to include future leaves linked to accruals into the total count of available leaves.
|
||||
# However, we'll need to check if those leaves take more than what will be accrued in total of those days
|
||||
# to give a warning if the total exceeds what will be accrued.
|
||||
if allocation.date_from > leave.date_to.date() or (allocation.date_to and allocation.date_to < leave.date_from.date()):
|
||||
continue
|
||||
interval_start = max(
|
||||
leave.date_from,
|
||||
datetime.combine(allocation.date_from, time.min)
|
||||
)
|
||||
interval_end = min(
|
||||
leave.date_to,
|
||||
datetime.combine(allocation.date_to, time.max)
|
||||
if allocation.date_to else leave.date_to
|
||||
)
|
||||
duration = leave[leave_duration_field]
|
||||
if leave.date_from != interval_start or leave.date_to != interval_end:
|
||||
duration_info = employee._get_calendar_attendances(interval_start.replace(tzinfo=pytz.UTC), interval_end.replace(tzinfo=pytz.UTC))
|
||||
duration = duration_info['hours' if leave_unit == 'hours' else 'days']
|
||||
max_allowed_duration = min(
|
||||
duration,
|
||||
leave_type_data[allocation]['virtual_remaining_leaves']
|
||||
)
|
||||
|
||||
if not max_allowed_duration:
|
||||
continue
|
||||
|
||||
allocated_time = min(max_allowed_duration, leave_duration)
|
||||
leave_type_data[allocation]['virtual_leaves_taken'] += allocated_time
|
||||
leave_type_data[allocation]['virtual_remaining_leaves'] -= allocated_time
|
||||
if leave.state == 'validate':
|
||||
leave_type_data[allocation]['leaves_taken'] += allocated_time
|
||||
leave_type_data[allocation]['remaining_leaves'] -= allocated_time
|
||||
|
||||
leave_duration -= allocated_time
|
||||
if not leave_duration:
|
||||
break
|
||||
if round(leave_duration, 2) > 0 and not skip_excess:
|
||||
to_recheck_leaves_per_leave_type[employee][leave_type]['excess_days'][leave.date_to.date()] = {
|
||||
'amount': leave_duration,
|
||||
'is_virtual': leave.state != 'validate',
|
||||
'leave_id': leave.id,
|
||||
}
|
||||
else:
|
||||
if leave_unit == 'hours':
|
||||
allocated_time = leave.number_of_hours
|
||||
else:
|
||||
allocated_time = leave.number_of_days
|
||||
leave_type_data[False]['virtual_leaves_taken'] += allocated_time
|
||||
leave_type_data[False]['virtual_remaining_leaves'] = 0
|
||||
leave_type_data[False]['remaining_leaves'] = 0
|
||||
if leave.state == 'validate':
|
||||
leave_type_data[False]['leaves_taken'] += allocated_time
|
||||
for employee in to_recheck_leaves_per_leave_type:
|
||||
for leave_type in to_recheck_leaves_per_leave_type[employee]:
|
||||
content = to_recheck_leaves_per_leave_type[employee][leave_type]
|
||||
consumed_content = allocations_leaves_consumed[employee][leave_type]
|
||||
if content['to_recheck_leaves']:
|
||||
date_to_simulate = max(content['to_recheck_leaves'].mapped('date_from')).date()
|
||||
latest_accrual_bonus = 0
|
||||
date_accrual_bonus = 0
|
||||
virtual_remaining = 0
|
||||
additional_leaves_duration = 0
|
||||
for allocation in consumed_content:
|
||||
latest_accrual_bonus += allocation and allocation._get_future_leaves_on(date_to_simulate)
|
||||
date_accrual_bonus += consumed_content[allocation]['accrual_bonus']
|
||||
virtual_remaining += consumed_content[allocation]['virtual_remaining_leaves']
|
||||
for leave in content['to_recheck_leaves']:
|
||||
additional_leaves_duration += leave.number_of_hours if leave_type.request_unit == 'hour' else leave.number_of_days
|
||||
latest_remaining = virtual_remaining - date_accrual_bonus + latest_accrual_bonus
|
||||
content['exceeding_duration'] = round(min(0, latest_remaining - additional_leaves_duration), 2)
|
||||
|
||||
return (allocations_leaves_consumed, to_recheck_leaves_per_leave_type)
|
||||
|
||||
def _get_hours_per_day(self, date_from):
|
||||
''' Return 24H to handle the case of Fully Flexible (ones without a working calendar)'''
|
||||
if not self:
|
||||
return 0
|
||||
calendars = self._get_calendars(date_from)
|
||||
return calendars[self.id].hours_per_day if calendars[self.id] else 24
|
||||
|
||||
def _get_store_avatar_card_fields(self, target):
|
||||
return [*super()._get_store_avatar_card_fields(target), "leave_date_to"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrEmployeePublic(models.Model):
|
||||
_inherit = 'hr.employee.public'
|
||||
|
||||
leave_manager_id = fields.Many2one(
|
||||
'res.users', string='Time Off Approver',
|
||||
compute='_compute_leave_manager', store=True, readonly=False,
|
||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
||||
help='Select the user responsible for approving "Time Off" of this employee.\n'
|
||||
'If empty, the approval is done by an Administrator or Approver (determined in settings/users).')
|
||||
leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
|
||||
show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves')
|
||||
is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee')
|
||||
allocation_display = fields.Char(compute='_compute_allocation_display')
|
||||
allocation_remaining_display = fields.Char(related='employee_id.allocation_remaining_display')
|
||||
|
||||
def _compute_show_leaves(self):
|
||||
self._compute_from_employee('show_leaves')
|
||||
|
||||
def _compute_leave_manager(self):
|
||||
self._compute_from_employee('leave_manager_id')
|
||||
|
||||
def _compute_leave_status(self):
|
||||
self._compute_from_employee(['leave_date_to', 'is_absent'])
|
||||
|
||||
def _search_absent_employee(self, operator, value):
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
# This search is only used for the 'Absent Today' filter however
|
||||
# this only returns employees that are absent right now.
|
||||
today_start = date.today()
|
||||
today_end = today_start + timedelta(1)
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', '!=', False),
|
||||
('state', '=', 'validate'),
|
||||
('date_from', '<', today_end),
|
||||
('date_to', '>=', today_start),
|
||||
])
|
||||
return [('id', 'in', holidays.employee_id.ids)]
|
||||
|
||||
def _compute_allocation_display(self):
|
||||
self._compute_from_employee('allocation_display')
|
||||
|
||||
def action_time_off_dashboard(self):
|
||||
self.ensure_one()
|
||||
if self.is_user:
|
||||
return self.employee_id.action_time_off_dashboard()
|
||||
|
||||
def action_open_time_off_calendar(self):
|
||||
"""Open the time off calendar filtered on this employee."""
|
||||
self.ensure_one()
|
||||
action = self.env.ref('hr_holidays.action_my_days_off_dashboard_calendar').sudo().read()[0]
|
||||
action['domain'] = [('employee_id', '=', self.id)]
|
||||
ctx = ({
|
||||
'active_employee_id': self.id,
|
||||
'search_default_employee_id': [self.id],
|
||||
'search_default_my_leaves': 0,
|
||||
'search_default_team': 0,
|
||||
'search_default_current_year': 1,
|
||||
'hide_employee_name': 1,
|
||||
})
|
||||
action['context'] = ctx
|
||||
return action
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,28 +1,67 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from calendar import monthrange
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.hr_holidays.models.hr_leave_accrual_plan_level import _get_selection_days
|
||||
|
||||
class AccrualPlan(models.Model):
|
||||
_name = "hr.leave.accrual.plan"
|
||||
|
||||
class HrLeaveAccrualPlan(models.Model):
|
||||
_name = 'hr.leave.accrual.plan'
|
||||
_description = "Accrual Plan"
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
name = fields.Char('Name', required=True)
|
||||
time_off_type_id = fields.Many2one('hr.leave.type', string="Time Off Type",
|
||||
check_company=True, index='btree_not_null',
|
||||
help="""Specify if this accrual plan can only be used with this Time Off Type.
|
||||
Leave empty if this accrual plan can be used with any Time Off Type.""")
|
||||
employees_count = fields.Integer("Employees", compute='_compute_employee_count')
|
||||
level_ids = fields.One2many('hr.leave.accrual.level', 'accrual_plan_id', copy=True)
|
||||
allocation_ids = fields.One2many('hr.leave.allocation', 'accrual_plan_id')
|
||||
level_ids = fields.One2many('hr.leave.accrual.level', 'accrual_plan_id', copy=True, string="Milestones")
|
||||
allocation_ids = fields.One2many('hr.leave.allocation', 'accrual_plan_id',
|
||||
export_string_translation=False)
|
||||
company_id = fields.Many2one('res.company', string='Company', domain=lambda self: [('id', 'in', self.env.companies.ids)],
|
||||
compute="_compute_company_id", store="True", readonly=False)
|
||||
transition_mode = fields.Selection([
|
||||
('immediately', 'Immediately'),
|
||||
('end_of_accrual', "After this accrual's period")],
|
||||
string="Level Transition", default="immediately", required=True,
|
||||
help="""Immediately: When the date corresponds to the new level, your accrual is automatically computed, granted and you switch to new level
|
||||
After this accrual's period: When the accrual is complete (a week, a month), and granted, you switch to next level if allocation date corresponds""")
|
||||
show_transition_mode = fields.Boolean(compute='_compute_show_transition_mode')
|
||||
export_string_translation=False, default="immediately", required=True)
|
||||
show_transition_mode = fields.Boolean(compute='_compute_show_transition_mode', export_string_translation=False)
|
||||
is_based_on_worked_time = fields.Boolean(compute="_compute_is_based_on_worked_time", store=True, readonly=False,
|
||||
export_string_translation=False,
|
||||
help="Only excludes requests where the time off type is set as unpaid kind of.")
|
||||
accrued_gain_time = fields.Selection([
|
||||
("start", "At the start of the accrual period"),
|
||||
("end", "At the end of the accrual period")],
|
||||
export_string_translation=False,
|
||||
default="end", required=True)
|
||||
can_be_carryover = fields.Boolean(export_string_translation=False)
|
||||
carryover_date = fields.Selection([
|
||||
("year_start", "At the start of the year"),
|
||||
("allocation", "At the allocation date"),
|
||||
("other", "Custom date")],
|
||||
export_string_translation=False,
|
||||
default="year_start", required=True, string="Carry-Over Time")
|
||||
carryover_day = fields.Selection(
|
||||
_get_selection_days, compute='_compute_carryover_day',
|
||||
export_string_translation=False, store=True, readonly=False, default='1')
|
||||
carryover_month = fields.Selection([
|
||||
("1", "January"),
|
||||
("2", "February"),
|
||||
("3", "March"),
|
||||
("4", "April"),
|
||||
("5", "May"),
|
||||
("6", "June"),
|
||||
("7", "July"),
|
||||
("8", "August"),
|
||||
("9", "September"),
|
||||
("10", "October"),
|
||||
("11", "November"),
|
||||
("12", "December")
|
||||
], export_string_translation=False, default=lambda self: str((fields.Date.today()).month))
|
||||
added_value_type = fields.Selection([('day', 'Days'), ('hour', 'Hours')],
|
||||
export_string_translation=False, default="day", store=True)
|
||||
|
||||
@api.depends('level_ids')
|
||||
def _compute_show_transition_mode(self):
|
||||
|
|
@ -35,10 +74,10 @@ class AccrualPlan(models.Model):
|
|||
def _compute_level_count(self):
|
||||
level_read_group = self.env['hr.leave.accrual.level']._read_group(
|
||||
[('accrual_plan_id', 'in', self.ids)],
|
||||
fields=['accrual_plan_id'],
|
||||
groupby=['accrual_plan_id'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
mapped_count = {group['accrual_plan_id'][0]: group['accrual_plan_id_count'] for group in level_read_group}
|
||||
mapped_count = {accrual_plan.id: count for accrual_plan, count in level_read_group}
|
||||
for plan in self:
|
||||
plan.level_count = mapped_count.get(plan.id, 0)
|
||||
|
||||
|
|
@ -46,29 +85,76 @@ class AccrualPlan(models.Model):
|
|||
def _compute_employee_count(self):
|
||||
allocations_read_group = self.env['hr.leave.allocation']._read_group(
|
||||
[('accrual_plan_id', 'in', self.ids)],
|
||||
['accrual_plan_id', 'employee_count:count_distinct(employee_id)'],
|
||||
['accrual_plan_id'],
|
||||
['employee_id:count_distinct'],
|
||||
)
|
||||
allocations_dict = {res['accrual_plan_id'][0]: res['employee_count'] for res in allocations_read_group}
|
||||
allocations_dict = {accrual_plan.id: count for accrual_plan, count in allocations_read_group}
|
||||
for plan in self:
|
||||
plan.employees_count = allocations_dict.get(plan.id, 0)
|
||||
|
||||
@api.depends('time_off_type_id.company_id')
|
||||
def _compute_company_id(self):
|
||||
for accrual_plan in self:
|
||||
if accrual_plan.time_off_type_id:
|
||||
accrual_plan.company_id = accrual_plan.time_off_type_id.company_id
|
||||
else:
|
||||
accrual_plan.company_id = self.env.company
|
||||
|
||||
@api.depends("accrued_gain_time")
|
||||
def _compute_is_based_on_worked_time(self):
|
||||
for plan in self:
|
||||
if plan.accrued_gain_time == "start":
|
||||
plan.is_based_on_worked_time = False
|
||||
|
||||
@api.depends("carryover_month")
|
||||
def _compute_carryover_day(self):
|
||||
for plan in self:
|
||||
# 2020 is a leap year, so monthrange(2020, february) will return [2, 29]
|
||||
plan.carryover_day = str(min(monthrange(2020, int(plan.carryover_month))[1], int(plan.carryover_day)))
|
||||
|
||||
def action_open_accrual_plan_employees(self):
|
||||
self.ensure_one()
|
||||
|
||||
return {
|
||||
'name': _("Accrual Plan's Employees"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'kanban,tree,form',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'res_model': 'hr.employee',
|
||||
'domain': [('id', 'in', self.allocation_ids.employee_id.ids)],
|
||||
}
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {},
|
||||
name=_("%s (copy)", self.name))
|
||||
return super().copy(default=default)
|
||||
def action_create_accrual_plan_level(self):
|
||||
return {
|
||||
'name': self.env._('New Milestone'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.leave.accrual.level',
|
||||
'view_mode': 'form',
|
||||
'views': [[False, 'form']],
|
||||
'view_id': self.env.ref('hr_holidays.hr_accrual_level_view_form').id,
|
||||
'target': 'new',
|
||||
'context': dict(
|
||||
self.env.context,
|
||||
new=True,
|
||||
default_can_be_carryover=self.can_be_carryover,
|
||||
default_accrued_gain_time=self.accrued_gain_time,
|
||||
default_can_modify_value_type=not self.time_off_type_id and not self.level_ids,
|
||||
default_added_value_type=self.added_value_type,
|
||||
),
|
||||
}
|
||||
|
||||
def action_open_accrual_plan_level(self, level_id):
|
||||
return {
|
||||
'name': self.env._('Milestone Edition'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.leave.accrual.level',
|
||||
'view_mode': 'form',
|
||||
'views': [[False, 'form']],
|
||||
'target': 'new',
|
||||
'res_id': level_id,
|
||||
}
|
||||
|
||||
def copy_data(self, default=None):
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, name=self.env._("%s (copy)", plan.name)) for plan, vals in zip(self, vals_list)]
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _prevent_used_plan_unlink(self):
|
||||
|
|
@ -81,3 +167,10 @@ class AccrualPlan(models.Model):
|
|||
raise ValidationError(_(
|
||||
"Some of the accrual plans you're trying to delete are linked to an existing allocation. Delete or cancel them first."
|
||||
))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get("name", False):
|
||||
vals['name'] = self.env._("Unnamed Plan")
|
||||
return super().create(vals_list)
|
||||
|
|
|
|||
|
|
@ -1,54 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
from calendar import monthrange
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools.date_utils import get_timedelta
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
||||
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
||||
# Used for displaying the days and reversing selection -> integer
|
||||
DAY_SELECT_VALUES = [str(i) for i in range(1, 29)] + ['last']
|
||||
DAY_SELECT_SELECTION_NO_LAST = tuple(zip(DAY_SELECT_VALUES, (str(i) for i in range(1, 29))))
|
||||
|
||||
def _get_selection_days(self):
|
||||
return DAY_SELECT_SELECTION_NO_LAST + (("last", _("last day")),)
|
||||
return [(str(i), str(i)) for i in range(1, 32)]
|
||||
|
||||
class AccrualPlanLevel(models.Model):
|
||||
_name = "hr.leave.accrual.level"
|
||||
|
||||
class HrLeaveAccrualLevel(models.Model):
|
||||
_name = 'hr.leave.accrual.level'
|
||||
_description = "Accrual Plan Level"
|
||||
_order = 'sequence asc'
|
||||
|
||||
sequence = fields.Integer(
|
||||
string='sequence', compute='_compute_sequence', store=True,
|
||||
help='Sequence is generated automatically by start time delta.')
|
||||
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True)
|
||||
start_count = fields.Integer(
|
||||
"Start after",
|
||||
help="The accrual starts after a defined period from the allocation start date. This field defines the number of days, months or years after which accrual is used.", default="1")
|
||||
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True, index=True, ondelete="cascade", default=lambda self: self.env.context.get("active_id", None))
|
||||
accrued_gain_time = fields.Selection(related='accrual_plan_id.accrued_gain_time', export_string_translation=False)
|
||||
start_count = fields.Integer(export_string_translation=False,
|
||||
help="The accrual starts after a defined period from the allocation start date. This field defines the number of days, months or years after which accrual is used.")
|
||||
start_type = fields.Selection(
|
||||
[('day', 'day(s)'),
|
||||
('month', 'month(s)'),
|
||||
('year', 'year(s)')],
|
||||
default='day', string=" ", required=True,
|
||||
[('day', 'Days'),
|
||||
('month', 'Months'),
|
||||
('year', 'Years')],
|
||||
default='day', required=True, export_string_translation=False,
|
||||
help="This field defines the unit of time after which the accrual starts.")
|
||||
is_based_on_worked_time = fields.Boolean("Based on worked time",
|
||||
help="If checked, the rate will be prorated on time off type where type is set on Working Time in the configuration.")
|
||||
|
||||
milestone_date = fields.Selection(
|
||||
[('creation', 'At allocation creation'),
|
||||
('after', 'After')],
|
||||
compute='_compute_milestone_date', inverse='_inverse_milestone_date', readonly=False,
|
||||
store=True, export_string_translation=False,
|
||||
default='creation', required=True
|
||||
)
|
||||
# Accrue of
|
||||
added_value = fields.Float(
|
||||
"Rate", digits=(16, 5), required=True,
|
||||
help="The number of hours/days that will be incremented in the specified Time Off Type for every period")
|
||||
added_value_type = fields.Selection(
|
||||
[('days', 'Days'),
|
||||
('hours', 'Hours')],
|
||||
default='days', required=True)
|
||||
added_value = fields.Float(digits=(16, 5), required=True, default=1, export_string_translation=False)
|
||||
added_value_type = fields.Selection([
|
||||
('day', 'Day(s)'),
|
||||
('hour', 'Hour(s)')
|
||||
], compute="_compute_added_value_type", inverse="_inverse_added_value_type", precompute=True, store=True, required=True,
|
||||
readonly=False, export_string_translation=False)
|
||||
frequency = fields.Selection([
|
||||
('hourly', 'Hourly'),
|
||||
('daily', 'Daily'),
|
||||
('weekly', 'Weekly'),
|
||||
('bimonthly', 'Twice a month'),
|
||||
|
|
@ -57,85 +52,136 @@ class AccrualPlanLevel(models.Model):
|
|||
('yearly', 'Yearly'),
|
||||
], default='daily', required=True, string="Frequency")
|
||||
week_day = fields.Selection([
|
||||
('mon', 'Monday'),
|
||||
('tue', 'Tuesday'),
|
||||
('wed', 'Wednesday'),
|
||||
('thu', 'Thursday'),
|
||||
('fri', 'Friday'),
|
||||
('sat', 'Saturday'),
|
||||
('sun', 'Sunday'),
|
||||
], default='mon', required=True, string="Allocation on")
|
||||
first_day = fields.Integer(default=1)
|
||||
first_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_day_display')
|
||||
second_day = fields.Integer(default=15)
|
||||
second_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_day_display')
|
||||
first_month_day = fields.Integer(default=1)
|
||||
first_month_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_month_day_display')
|
||||
('0', 'Monday'),
|
||||
('1', 'Tuesday'),
|
||||
('2', 'Wednesday'),
|
||||
('3', 'Thursday'),
|
||||
('4', 'Friday'),
|
||||
('5', 'Saturday'),
|
||||
('6', 'Sunday'),
|
||||
], default='0', required=True, string="Allocation on")
|
||||
first_day = fields.Selection(_get_selection_days, default='1', export_string_translation=False)
|
||||
second_day = fields.Selection(_get_selection_days, default='15', export_string_translation=False)
|
||||
first_month_day = fields.Selection(
|
||||
_get_selection_days, compute='_compute_first_month_day', store=True, readonly=False, default='1',
|
||||
export_string_translation=False)
|
||||
first_month = fields.Selection([
|
||||
('jan', 'January'),
|
||||
('feb', 'February'),
|
||||
('mar', 'March'),
|
||||
('apr', 'April'),
|
||||
('may', 'May'),
|
||||
('jun', 'June'),
|
||||
], default="jan")
|
||||
second_month_day = fields.Integer(default=1)
|
||||
second_month_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_month_day_display')
|
||||
('1', 'January'),
|
||||
('2', 'February'),
|
||||
('3', 'March'),
|
||||
('4', 'April'),
|
||||
('5', 'May'),
|
||||
('6', 'June'),
|
||||
], default="1", export_string_translation=False)
|
||||
second_month_day = fields.Selection(
|
||||
_get_selection_days, compute='_compute_second_month_day', store=True, readonly=False, default='1',
|
||||
export_string_translation=False)
|
||||
second_month = fields.Selection([
|
||||
('jul', 'July'),
|
||||
('aug', 'August'),
|
||||
('sep', 'September'),
|
||||
('oct', 'October'),
|
||||
('nov', 'November'),
|
||||
('dec', 'December')
|
||||
], default="jul")
|
||||
('7', 'July'),
|
||||
('8', 'August'),
|
||||
('9', 'September'),
|
||||
('10', 'October'),
|
||||
('11', 'November'),
|
||||
('12', 'December')
|
||||
], default="7", export_string_translation=False)
|
||||
yearly_month = fields.Selection([
|
||||
('jan', 'January'),
|
||||
('feb', 'February'),
|
||||
('mar', 'March'),
|
||||
('apr', 'April'),
|
||||
('may', 'May'),
|
||||
('jun', 'June'),
|
||||
('jul', 'July'),
|
||||
('aug', 'August'),
|
||||
('sep', 'September'),
|
||||
('oct', 'October'),
|
||||
('nov', 'November'),
|
||||
('dec', 'December')
|
||||
], default="jan")
|
||||
yearly_day = fields.Integer(default=1)
|
||||
yearly_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_yearly_day_display')
|
||||
('1', 'January'),
|
||||
('2', 'February'),
|
||||
('3', 'March'),
|
||||
('4', 'April'),
|
||||
('5', 'May'),
|
||||
('6', 'June'),
|
||||
('7', 'July'),
|
||||
('8', 'August'),
|
||||
('9', 'September'),
|
||||
('10', 'October'),
|
||||
('11', 'November'),
|
||||
('12', 'December')
|
||||
], default="1", export_string_translation=False)
|
||||
yearly_day = fields.Selection(
|
||||
_get_selection_days, compute='_compute_yearly_day', store=True, readonly=False, default='1',
|
||||
export_string_translation=False)
|
||||
cap_accrued_time = fields.Boolean(export_string_translation=False,
|
||||
help="When the field is checked the balance of an allocation using this accrual plan will never exceed the specified amount.")
|
||||
maximum_leave = fields.Float(
|
||||
'Limit to', required=False, default=100,
|
||||
help="Choose a cap for this accrual. 0 means no cap.")
|
||||
parent_id = fields.Many2one(
|
||||
'hr.leave.accrual.level', string="Previous Level",
|
||||
help="If this field is empty, this level is the first one.")
|
||||
digits=(16, 2), compute="_compute_maximum_leave", default=0, readonly=False, store=True,
|
||||
help="Choose a cap for this accrual.", export_string_translation=False)
|
||||
cap_accrued_time_yearly = fields.Boolean(export_string_translation=False,
|
||||
store=True, readonly=False,
|
||||
help="When the field is checked the total amount accrued each year will be capped at the specified amount")
|
||||
maximum_leave_yearly = fields.Float(digits=(16, 2), export_string_translation=False)
|
||||
can_be_carryover = fields.Boolean(related='accrual_plan_id.can_be_carryover', readonly=True,
|
||||
export_string_translation=False)
|
||||
action_with_unused_accruals = fields.Selection(
|
||||
[('postponed', 'Transferred to the next year'),
|
||||
('lost', 'Lost')],
|
||||
string="At the end of the calendar year, unused accruals will be",
|
||||
default='postponed', required='True')
|
||||
postpone_max_days = fields.Integer("Maximum amount of accruals to transfer",
|
||||
help="Set a maximum of days an allocation keeps at the end of the year. 0 for no limit.")
|
||||
[('lost', 'Lost'),
|
||||
('all', 'Carried over')],
|
||||
compute="_compute_action_with_unused_accruals",
|
||||
store=True,
|
||||
export_string_translation=False,
|
||||
default='lost', required=True,
|
||||
help="When the Carry-Over Time is reached, according to Plan's setting, select what you want "
|
||||
"to happen with the unused time off: Lost (time will be reset to zero), Carried over (accrued time carried over to "
|
||||
"the next period.)")
|
||||
carryover_options = fields.Selection(
|
||||
[('unlimited', 'Unlimited'),
|
||||
('limited', 'Up to')],
|
||||
store=True, readonly=False,
|
||||
export_string_translation=False,
|
||||
compute="_compute_carryover_options",
|
||||
default='unlimited', required=True,
|
||||
help="You can limit the accrued time carried over for the next period."
|
||||
)
|
||||
postpone_max_days = fields.Integer(export_string_translation=False,
|
||||
help="Set a maximum of accruals an allocation keeps at the end of the year.")
|
||||
can_modify_value_type = fields.Boolean(compute="_compute_can_modify_value_type", default=False,
|
||||
export_string_translation=False)
|
||||
accrual_validity = fields.Boolean(export_string_translation=False, compute="_compute_accrual_validity", store=True, readonly=False)
|
||||
accrual_validity_count = fields.Integer(
|
||||
export_string_translation=False,
|
||||
help="You can define a period of time where the days carried over will be available", default="1")
|
||||
accrual_validity_type = fields.Selection(
|
||||
[('day', 'Days'),
|
||||
('month', 'Months')],
|
||||
default='day', export_string_translation=False, required=True,
|
||||
help="This field defines the unit of time after which the accrual ends.")
|
||||
|
||||
_sql_constraints = [
|
||||
('check_dates',
|
||||
"CHECK( (frequency = 'daily') or"
|
||||
"(week_day IS NOT NULL AND frequency = 'weekly') or "
|
||||
"(first_day > 0 AND second_day > first_day AND first_day <= 31 AND second_day <= 31 AND frequency = 'bimonthly') or "
|
||||
"(first_day > 0 AND first_day <= 31 AND frequency = 'monthly')or "
|
||||
"(first_month_day > 0 AND first_month_day <= 31 AND second_month_day > 0 AND second_month_day <= 31 AND frequency = 'biyearly') or "
|
||||
"(yearly_day > 0 AND yearly_day <= 31 AND frequency = 'yearly'))",
|
||||
"The dates you've set up aren't correct. Please check them."),
|
||||
('start_count_check', "CHECK( start_count >= 0 )", "You can not start an accrual in the past."),
|
||||
('added_value_greater_than_zero', 'CHECK(added_value > 0)', 'You must give a rate greater than 0 in accrual plan levels.')
|
||||
]
|
||||
_start_count_check = models.Constraint(
|
||||
"CHECK((start_count > 0 AND milestone_date = 'after') OR (start_count = 0 AND milestone_date = 'creation'))",
|
||||
'You can not start an accrual in the past.',
|
||||
)
|
||||
_added_value_greater_than_zero = models.Constraint(
|
||||
'CHECK(added_value > 0)',
|
||||
'You must give a rate greater than 0 in accrual plan levels.',
|
||||
)
|
||||
_valid_postpone_max_days_value = models.Constraint(
|
||||
"CHECK(action_with_unused_accruals <> 'all' OR carryover_options <> 'limited' OR COALESCE(postpone_max_days, 0) > 0)",
|
||||
'You cannot have a maximum quantity to carryover set to 0.',
|
||||
)
|
||||
_valid_accrual_validity_value = models.Constraint(
|
||||
'CHECK(accrual_validity IS NOT TRUE OR COALESCE(accrual_validity_count, 0) > 0)',
|
||||
'You cannot have an accrual validity time set to 0.',
|
||||
)
|
||||
_valid_yearly_cap_value = models.Constraint(
|
||||
'CHECK(cap_accrued_time_yearly IS NOT TRUE OR COALESCE(maximum_leave_yearly, 0) > 0)',
|
||||
'You cannot have a cap on yearly accrued time without setting a maximum amount.',
|
||||
)
|
||||
|
||||
@api.constrains('first_day', 'second_day', 'week_day', 'frequency')
|
||||
def _check_dates(self):
|
||||
error_message = ''
|
||||
for level in self:
|
||||
if level.frequency == 'weekly' and not level.week_day:
|
||||
error_message = _("Weekday must be selected to use the frequency weekly")
|
||||
elif level.frequency == 'bimonthly' and int(level.first_day) >= int(level.second_day):
|
||||
error_message = _("The first day must be lower than the second day.")
|
||||
if error_message:
|
||||
raise ValidationError(error_message)
|
||||
|
||||
@api.constrains('cap_accrued_time', 'maximum_leave')
|
||||
def _check_maximum_leaves(self):
|
||||
for level in self:
|
||||
if level.cap_accrued_time and level.maximum_leave <= 0:
|
||||
raise UserError(self.env._("You cannot have a balance cap on accrued time set to 0."))
|
||||
|
||||
@api.depends('start_count', 'start_type')
|
||||
def _compute_sequence(self):
|
||||
|
|
@ -148,97 +194,124 @@ class AccrualPlanLevel(models.Model):
|
|||
for level in self:
|
||||
level.sequence = level.start_count * start_type_multipliers[level.start_type]
|
||||
|
||||
@api.depends('first_day', 'second_day', 'first_month_day', 'second_month_day', 'yearly_day')
|
||||
def _compute_days_display(self):
|
||||
days_select = _get_selection_days(self)
|
||||
@api.depends('accrual_plan_id', 'accrual_plan_id.level_ids', 'accrual_plan_id.time_off_type_id')
|
||||
def _compute_can_modify_value_type(self):
|
||||
for level in self:
|
||||
level.first_day_display = days_select[min(level.first_day - 1, 28)][0]
|
||||
level.second_day_display = days_select[min(level.second_day - 1, 28)][0]
|
||||
level.first_month_day_display = days_select[min(level.first_month_day - 1, 28)][0]
|
||||
level.second_month_day_display = days_select[min(level.second_month_day - 1, 28)][0]
|
||||
level.yearly_day_display = days_select[min(level.yearly_day - 1, 28)][0]
|
||||
level.can_modify_value_type = not level.accrual_plan_id.time_off_type_id and level.accrual_plan_id.level_ids and level.accrual_plan_id.level_ids[0] == level
|
||||
|
||||
def _inverse_first_day_display(self):
|
||||
def _inverse_added_value_type(self):
|
||||
for level in self:
|
||||
if level.first_day_display == 'last':
|
||||
level.first_day = 31
|
||||
else:
|
||||
level.first_day = DAY_SELECT_VALUES.index(level.first_day_display) + 1
|
||||
if level.accrual_plan_id.level_ids[0] == level:
|
||||
level.accrual_plan_id.added_value_type = level.added_value_type
|
||||
|
||||
def _inverse_second_day_display(self):
|
||||
@api.depends('accrual_plan_id', 'accrual_plan_id.level_ids', 'accrual_plan_id.added_value_type', 'accrual_plan_id.time_off_type_id')
|
||||
def _compute_added_value_type(self):
|
||||
for level in self:
|
||||
if level.second_day_display == 'last':
|
||||
level.second_day = 31
|
||||
else:
|
||||
level.second_day = DAY_SELECT_VALUES.index(level.second_day_display) + 1
|
||||
if level.accrual_plan_id.time_off_type_id:
|
||||
level.added_value_type = "day" if level.accrual_plan_id.time_off_type_id.request_unit in ["day", "half_day"] else "hour"
|
||||
elif level.accrual_plan_id.level_ids and level.accrual_plan_id.level_ids[0] != level:
|
||||
level.added_value_type = level.accrual_plan_id.level_ids[0].added_value_type
|
||||
elif not level.added_value_type:
|
||||
level.added_value_type = "day" # default value
|
||||
|
||||
def _inverse_first_month_day_display(self):
|
||||
def _set_day(self, day_field, month_field):
|
||||
for level in self:
|
||||
if level.first_month_day_display == 'last':
|
||||
level.first_month_day = 31
|
||||
else:
|
||||
level.first_month_day = DAY_SELECT_VALUES.index(level.first_month_day_display) + 1
|
||||
# 2020 is a leap year, so monthrange(2020, february) will return [2, 29]
|
||||
level[day_field] = str(min(monthrange(2020, int(level[month_field]))[1], int(level[day_field])))
|
||||
|
||||
def _inverse_second_month_day_display(self):
|
||||
for level in self:
|
||||
if level.second_month_day_display == 'last':
|
||||
level.second_month_day = 31
|
||||
else:
|
||||
level.second_month_day = DAY_SELECT_VALUES.index(level.second_month_day_display) + 1
|
||||
@api.depends("first_month")
|
||||
def _compute_first_month_day(self):
|
||||
self._set_day("first_month_day", "first_month")
|
||||
|
||||
def _inverse_yearly_day_display(self):
|
||||
@api.depends("second_month")
|
||||
def _compute_second_month_day(self):
|
||||
self._set_day("second_month_day", "second_month")
|
||||
|
||||
@api.depends("yearly_month")
|
||||
def _compute_yearly_day(self):
|
||||
self._set_day("yearly_day", "yearly_month")
|
||||
|
||||
@api.depends('cap_accrued_time')
|
||||
def _compute_maximum_leave(self):
|
||||
for level in self:
|
||||
if level.yearly_day_display == 'last':
|
||||
level.yearly_day = 31
|
||||
else:
|
||||
level.yearly_day = DAY_SELECT_VALUES.index(level.yearly_day_display) + 1
|
||||
if not level.cap_accrued_time:
|
||||
level.maximum_leave = 0
|
||||
|
||||
@api.depends('can_be_carryover')
|
||||
def _compute_action_with_unused_accruals(self):
|
||||
for level in self:
|
||||
if not level.can_be_carryover:
|
||||
level.action_with_unused_accruals = 'lost'
|
||||
|
||||
@api.depends('action_with_unused_accruals')
|
||||
def _compute_carryover_options(self):
|
||||
for level in self:
|
||||
if level.action_with_unused_accruals == 'lost':
|
||||
level.carryover_options = 'unlimited'
|
||||
|
||||
@api.depends('action_with_unused_accruals')
|
||||
def _compute_accrual_validity(self):
|
||||
for level in self:
|
||||
if level.action_with_unused_accruals == 'lost':
|
||||
level.accrual_validity = False
|
||||
|
||||
@api.depends('start_count', 'milestone_date')
|
||||
def _compute_milestone_date(self):
|
||||
for level in self:
|
||||
if level.start_count == 0:
|
||||
level.milestone_date = 'creation'
|
||||
|
||||
def _inverse_milestone_date(self):
|
||||
for level in self:
|
||||
if level.milestone_date == 'creation':
|
||||
level.start_count = 0
|
||||
|
||||
def _get_hourly_frequencies(self):
|
||||
return ['hourly']
|
||||
|
||||
def _get_next_date(self, last_call):
|
||||
"""
|
||||
Returns the next date with the given last call
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.frequency == 'daily':
|
||||
if self.frequency in self._get_hourly_frequencies() + ['daily']:
|
||||
return last_call + relativedelta(days=1)
|
||||
elif self.frequency == 'weekly':
|
||||
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
weekday = daynames.index(self.week_day)
|
||||
return last_call + relativedelta(days=1, weekday=weekday)
|
||||
elif self.frequency == 'bimonthly':
|
||||
first_date = last_call + relativedelta(day=self.first_day)
|
||||
second_date = last_call + relativedelta(day=self.second_day)
|
||||
|
||||
if self.frequency == 'weekly':
|
||||
return last_call + relativedelta(days=1, weekday=int(self.week_day))
|
||||
|
||||
if self.frequency == 'bimonthly':
|
||||
first_date = last_call + relativedelta(day=int(self.first_day))
|
||||
second_date = last_call + relativedelta(day=int(self.second_day))
|
||||
if last_call < first_date:
|
||||
return first_date
|
||||
elif last_call < second_date:
|
||||
if last_call < second_date:
|
||||
return second_date
|
||||
else:
|
||||
return last_call + relativedelta(months=1, day=self.first_day)
|
||||
elif self.frequency == 'monthly':
|
||||
date = last_call + relativedelta(day=self.first_day)
|
||||
return last_call + relativedelta(day=int(self.first_day), months=1)
|
||||
|
||||
if self.frequency == 'monthly':
|
||||
date = last_call + relativedelta(day=int(self.first_day))
|
||||
if last_call < date:
|
||||
return date
|
||||
else:
|
||||
return last_call + relativedelta(months=1, day=self.first_day)
|
||||
elif self.frequency == 'biyearly':
|
||||
first_month = MONTHS.index(self.first_month) + 1
|
||||
second_month = MONTHS.index(self.second_month) + 1
|
||||
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
|
||||
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
|
||||
return last_call + relativedelta(day=int(self.first_day), months=1)
|
||||
|
||||
if self.frequency == 'biyearly':
|
||||
first_date = last_call + relativedelta(month=int(self.first_month), day=int(self.first_month_day))
|
||||
second_date = last_call + relativedelta(month=int(self.second_month), day=int(self.second_month_day))
|
||||
if last_call < first_date:
|
||||
return first_date
|
||||
elif last_call < second_date:
|
||||
if last_call < second_date:
|
||||
return second_date
|
||||
else:
|
||||
return last_call + relativedelta(years=1, month=first_month, day=self.first_month_day)
|
||||
elif self.frequency == 'yearly':
|
||||
month = MONTHS.index(self.yearly_month) + 1
|
||||
date = last_call + relativedelta(month=month, day=self.yearly_day)
|
||||
return last_call + relativedelta(month=int(self.first_month), day=int(self.first_month_day), years=1)
|
||||
|
||||
if self.frequency == 'yearly':
|
||||
date = last_call + relativedelta(month=int(self.yearly_month), day=int(self.yearly_day))
|
||||
if last_call < date:
|
||||
return date
|
||||
else:
|
||||
return last_call + relativedelta(years=1, month=month, day=self.yearly_day)
|
||||
else:
|
||||
return False
|
||||
return last_call + relativedelta(month=int(self.yearly_month), day=int(self.yearly_day), years=1)
|
||||
|
||||
raise ValidationError(_("Your frequency selection is not correct: please choose a frequency between theses options:"
|
||||
"Hourly, Daily, Weekly, Twice a month, Monthly, Twice a year and Yearly."))
|
||||
|
||||
def _get_previous_date(self, last_call):
|
||||
"""
|
||||
|
|
@ -247,44 +320,52 @@ class AccrualPlanLevel(models.Model):
|
|||
Contrary to `_get_next_date` this function will return the 01/02 if that date is given
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.frequency == 'daily':
|
||||
if self.frequency in self._get_hourly_frequencies() + ['daily']:
|
||||
return last_call
|
||||
elif self.frequency == 'weekly':
|
||||
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
weekday = daynames.index(self.week_day)
|
||||
return last_call + relativedelta(days=-6, weekday=weekday)
|
||||
elif self.frequency == 'bimonthly':
|
||||
second_date = last_call + relativedelta(day=self.second_day)
|
||||
first_date = last_call + relativedelta(day=self.first_day)
|
||||
|
||||
if self.frequency == 'weekly':
|
||||
return last_call + relativedelta(days=-6, weekday=int(self.week_day))
|
||||
|
||||
if self.frequency == 'bimonthly':
|
||||
first_date = last_call + relativedelta(day=int(self.first_day))
|
||||
second_date = last_call + relativedelta(day=int(self.second_day))
|
||||
if last_call >= second_date:
|
||||
return second_date
|
||||
elif last_call >= first_date:
|
||||
if last_call >= first_date:
|
||||
return first_date
|
||||
else:
|
||||
return last_call + relativedelta(months=-1, day=self.second_day)
|
||||
elif self.frequency == 'monthly':
|
||||
date = last_call + relativedelta(day=self.first_day)
|
||||
return last_call + relativedelta(day=int(self.second_day), months=-1)
|
||||
|
||||
if self.frequency == 'monthly':
|
||||
date = last_call + relativedelta(day=int(self.first_day))
|
||||
if last_call >= date:
|
||||
return date
|
||||
else:
|
||||
return last_call + relativedelta(months=-1, day=self.first_day)
|
||||
elif self.frequency == 'biyearly':
|
||||
first_month = MONTHS.index(self.first_month) + 1
|
||||
second_month = MONTHS.index(self.second_month) + 1
|
||||
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
|
||||
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
|
||||
return last_call + relativedelta(day=int(self.first_day), months=-1, days=1)
|
||||
|
||||
if self.frequency == 'biyearly':
|
||||
first_date = last_call + relativedelta(month=int(self.first_month), day=int(self.first_month_day))
|
||||
second_date = last_call + relativedelta(month=int(self.second_month), day=int(self.second_month_day))
|
||||
if last_call >= second_date:
|
||||
return second_date
|
||||
elif last_call >= first_date:
|
||||
if last_call >= first_date:
|
||||
return first_date
|
||||
else:
|
||||
return last_call + relativedelta(years=-1, month=second_month, day=self.second_month_day)
|
||||
elif self.frequency == 'yearly':
|
||||
month = MONTHS.index(self.yearly_month) + 1
|
||||
year_date = last_call + relativedelta(month=month, day=self.yearly_day)
|
||||
return last_call + relativedelta(month=int(self.second_month), day=int(self.second_month_day), years=-1)
|
||||
|
||||
if self.frequency == 'yearly':
|
||||
year_date = last_call + relativedelta(month=int(self.yearly_month), day=int(self.yearly_day))
|
||||
if last_call >= year_date:
|
||||
return year_date
|
||||
else:
|
||||
return last_call + relativedelta(years=-1, month=month, day=self.yearly_day)
|
||||
else:
|
||||
return False
|
||||
return last_call + relativedelta(month=int(self.yearly_month), day=int(self.yearly_day), years=-1)
|
||||
|
||||
raise ValidationError(_("Your frequency selection is not correct: please choose a frequency between theses options:"
|
||||
"Hourly, Daily, Weekly, Twice a month, Monthly, Twice a year and Yearly."))
|
||||
|
||||
def _get_level_transition_date(self, allocation_start):
|
||||
if self.start_type == 'day':
|
||||
return allocation_start + relativedelta(days=self.start_count)
|
||||
if self.start_type == 'month':
|
||||
return allocation_start + relativedelta(months=self.start_count)
|
||||
if self.start_type == 'year':
|
||||
return allocation_start + relativedelta(years=self.start_count)
|
||||
|
||||
def action_save_new(self):
|
||||
return self.accrual_plan_id.action_create_accrual_plan_level()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,12 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from random import randint
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StressDay(models.Model):
|
||||
_name = 'hr.leave.stress.day'
|
||||
_description = 'Stress Day'
|
||||
class HrLeaveMandatoryDay(models.Model):
|
||||
_name = 'hr.leave.mandatory.day'
|
||||
_description = 'Mandatory Day'
|
||||
_order = 'start_date desc, end_date desc'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
|
|
@ -18,7 +18,9 @@ class StressDay(models.Model):
|
|||
'resource.calendar', 'Working Hours',
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
department_ids = fields.Many2many('hr.department', string="Departments")
|
||||
job_ids = fields.Many2many('hr.job', string="Job Position")
|
||||
|
||||
_sql_constraints = [
|
||||
('date_from_after_day_to', 'CHECK(start_date <= end_date)', 'The start date must be anterior than the end date.')
|
||||
]
|
||||
_date_from_after_day_to = models.Constraint(
|
||||
'CHECK(start_date <= end_date)',
|
||||
'The start date must be anterior than the end date.',
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,185 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date
|
||||
from odoo import api, fields, models
|
||||
from odoo.fields import Domain
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
""" Write and Create:
|
||||
Special case when setting a contract as running:
|
||||
If there is already a validated time off over another contract
|
||||
with a different schedule, split the time off, before the
|
||||
_check_contracts raises an issue.
|
||||
If there are existing leaves that are spanned by this new
|
||||
contract, update their resource calendar to the current one.
|
||||
"""
|
||||
# TODO BIOUTIFY ME (the whole file :)
|
||||
_inherit = 'hr.version'
|
||||
_description = 'Employee Contract'
|
||||
|
||||
@api.constrains('contract_date_start', 'contract_date_end')
|
||||
def _check_contracts(self):
|
||||
self._get_leaves()._check_contracts()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
all_new_leave_origin = []
|
||||
all_new_leave_vals = []
|
||||
leaves_state = {}
|
||||
created_versions = self.env['hr.version']
|
||||
for vals in vals_list:
|
||||
if not 'employee_id' in vals or not 'resource_calendar_id' in vals:
|
||||
created_versions |= super().create(vals)
|
||||
continue
|
||||
leaves = self._get_leaves_from_vals(vals)
|
||||
is_created = False
|
||||
for leave in leaves:
|
||||
leaves_state = self._refuse_leave(leave, leaves_state) if leave.request_date_from < vals['contract_date_start'] else self._set_leave_draft(leave, leaves_state)
|
||||
if not is_created:
|
||||
created_versions |= super().create([vals])
|
||||
is_created = True
|
||||
overlapping_contracts = self._check_overlapping_contract(leave)
|
||||
if not overlapping_contracts:
|
||||
# When the leave is set to draft
|
||||
leave._compute_date_from_to()
|
||||
continue
|
||||
all_new_leave_origin, all_new_leave_vals = self._populate_all_new_leave_vals_from_split_leave(
|
||||
all_new_leave_origin, all_new_leave_vals, overlapping_contracts, leave, leaves_state)
|
||||
# TODO FIXME
|
||||
# to keep creation order, not ideal but ok for now.
|
||||
if not is_created:
|
||||
created_versions |= super().create([vals])
|
||||
try:
|
||||
if all_new_leave_vals:
|
||||
self._create_all_new_leave(all_new_leave_origin, all_new_leave_vals)
|
||||
except ValidationError:
|
||||
# In case a validation error is thrown due to holiday creation with the new resource calendar (which can
|
||||
# increase their duration), we catch this error to display a more meaningful error message.
|
||||
raise ValidationError(
|
||||
self.env._("Changing the contract on this employee changes their working schedule in a period "
|
||||
"they already took leaves. Changing this working schedule changes the duration of "
|
||||
"these leaves in such a way the employee no longer has the required allocation for "
|
||||
"them. Please review these leaves and/or allocations before changing the contract."))
|
||||
return created_versions
|
||||
|
||||
def write(self, vals):
|
||||
specific_contracts = self.env['hr.version']
|
||||
if any(field in vals for field in ['contract_date_start', 'contract_date_end', 'date_version', 'resource_calendar_id']):
|
||||
all_new_leave_origin = []
|
||||
all_new_leave_vals = []
|
||||
leaves_state = {}
|
||||
try:
|
||||
for contract in self:
|
||||
resource_calendar_id = vals.get('resource_calendar_id', contract.resource_calendar_id.id)
|
||||
extra_domain = [('resource_calendar_id', '!=', resource_calendar_id)] if resource_calendar_id else None
|
||||
leaves = contract._get_leaves(
|
||||
extra_domain=extra_domain
|
||||
)
|
||||
for leave in leaves:
|
||||
super(HrVersion, contract).write(vals)
|
||||
overlapping_contracts = self._check_overlapping_contract(leave)
|
||||
if not overlapping_contracts:
|
||||
continue
|
||||
leaves_state = self._refuse_leave(leave, leaves_state)
|
||||
specific_contracts += contract
|
||||
all_new_leave_origin, all_new_leave_vals = self._populate_all_new_leave_vals_from_split_leave(
|
||||
all_new_leave_origin, all_new_leave_vals, overlapping_contracts, leave, leaves_state)
|
||||
if all_new_leave_vals:
|
||||
self._create_all_new_leave(all_new_leave_origin, all_new_leave_vals)
|
||||
except ValidationError:
|
||||
# In case a validation error is thrown due to holiday creation with the new resource calendar (which can
|
||||
# increase their duration), we catch this error to display a more meaningful error message.
|
||||
raise ValidationError(self.env._("Changing the contract on this employee changes their working schedule in a period "
|
||||
"they already took leaves. Changing this working schedule changes the duration of "
|
||||
"these leaves in such a way the employee no longer has the required allocation for "
|
||||
"them. Please review these leaves and/or allocations before changing the contract."))
|
||||
return super(HrVersion, self - specific_contracts).write(vals)
|
||||
|
||||
def _get_leaves(self, extra_domain=None):
|
||||
domain = [
|
||||
('state', '!=', 'refuse'),
|
||||
('employee_id', 'in', self.mapped('employee_id.id')),
|
||||
('date_from', '<=', max(end or date.max for end in self.sudo().mapped('contract_date_end'))),
|
||||
('date_to', '>=', min(self.sudo().mapped('contract_date_start'))),
|
||||
]
|
||||
if extra_domain:
|
||||
domain = Domain.AND([domain, extra_domain])
|
||||
return self.env['hr.leave'].search(domain)
|
||||
|
||||
def _get_leaves_from_vals(self, vals):
|
||||
domain = [
|
||||
('state', '!=', 'refuse'),
|
||||
('employee_id', 'in', vals['employee_id']),
|
||||
('date_to', '>=', fields.Date.from_string(vals.get('contract_date_start', vals.get('date_version', fields.Date.today())))),
|
||||
('resource_calendar_id', '!=', vals.get('resource_calendar_id')),
|
||||
]
|
||||
if vals.get('contract_date_end'):
|
||||
domain = Domain.AND([domain, [('date_from', '<=', fields.Date.from_string(vals['contract_date_end']))]])
|
||||
return self.env['hr.leave'].search(domain)
|
||||
|
||||
def _check_overlapping_contract(self, leave):
|
||||
# Get all overlapping contracts but exclude draft contracts that are not included in this transaction.
|
||||
overlapping_contracts = leave._get_overlapping_contracts().sorted(
|
||||
key=lambda c: c.contract_date_start)
|
||||
if len(overlapping_contracts.resource_calendar_id) <= 1:
|
||||
if overlapping_contracts:
|
||||
first_overlapping_contract = next(iter(overlapping_contracts), overlapping_contracts)
|
||||
if leave.resource_calendar_id != first_overlapping_contract.resource_calendar_id:
|
||||
leave.resource_calendar_id = first_overlapping_contract.resource_calendar_id
|
||||
if not leave.request_unit_hours:
|
||||
leave.with_context(leave_skip_date_check=True, leave_skip_state_check=True)._compute_date_from_to()
|
||||
if leave.state == 'validate':
|
||||
leave._validate_leave_request()
|
||||
return False
|
||||
return overlapping_contracts
|
||||
|
||||
def _refuse_leave(self, leave, leaves_state):
|
||||
if leave.id not in leaves_state:
|
||||
leaves_state[leave.id] = leave.state
|
||||
if leave.state not in ['refuse', 'confirm']:
|
||||
leave.action_refuse()
|
||||
return leaves_state
|
||||
|
||||
def _set_leave_draft(self, leave, leaves_state):
|
||||
if leave.id not in leaves_state:
|
||||
leaves_state[leave.id] = leave.state
|
||||
if leave.state not in ['refuse', 'confirm']:
|
||||
leave.action_back_to_approval()
|
||||
return leaves_state
|
||||
|
||||
def _populate_all_new_leave_vals_from_split_leave(self, all_new_leave_origin, all_new_leave_vals, overlapping_contracts, leave, leaves_state):
|
||||
last_version = overlapping_contracts[-1]
|
||||
for overlapping_contract in overlapping_contracts:
|
||||
new_request_date_from = max(leave.request_date_from, overlapping_contract.contract_date_start)
|
||||
new_request_date_to = min(leave.request_date_to, overlapping_contract.contract_date_end or date.max)
|
||||
new_leave_vals = leave.copy_data({
|
||||
'request_date_from': new_request_date_from,
|
||||
'request_date_to': new_request_date_to,
|
||||
'state': leaves_state[leave.id] if overlapping_contract.id != last_version.id else 'confirm',
|
||||
})[0]
|
||||
new_leave = self.env['hr.leave'].new(new_leave_vals)
|
||||
new_leave._compute_date_from_to()
|
||||
new_leave._compute_duration()
|
||||
# Could happen for part-time contract, that time off is not necessary
|
||||
# anymore.
|
||||
if new_leave.date_from < new_leave.date_to:
|
||||
all_new_leave_origin.append(leave)
|
||||
all_new_leave_vals.append(new_leave._convert_to_write(new_leave._cache))
|
||||
return all_new_leave_origin, all_new_leave_vals
|
||||
|
||||
def _create_all_new_leave(self, all_new_leave_origin, all_new_leave_vals):
|
||||
new_leaves = self.env['hr.leave'].with_context(
|
||||
tracking_disable=True,
|
||||
mail_activity_automation_skip=True,
|
||||
leave_fast_create=True,
|
||||
leave_skip_state_check=True
|
||||
).create(all_new_leave_vals)
|
||||
new_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request()
|
||||
for index, new_leave in enumerate(new_leaves):
|
||||
new_leave.message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
render_values={'self': new_leave, 'origin': all_new_leave_origin[index]},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
from odoo import api, models
|
||||
|
||||
|
||||
class MailActivityType(models.Model):
|
||||
_inherit = "mail.activity.type"
|
||||
|
||||
@api.model
|
||||
def _get_model_info_by_xmlid(self):
|
||||
info = super()._get_model_info_by_xmlid()
|
||||
info['hr_holidays.mail_act_leave_approval'] = {'res_model': 'hr.leave', 'unlink': False}
|
||||
info['hr_holidays.mail_act_leave_second_approval'] = {'res_model': 'hr.leave', 'unlink': False}
|
||||
info['hr_holidays.mail_act_leave_allocation_approval'] = {'res_model': 'hr.leave.allocation', 'unlink': False}
|
||||
info['hr_holidays.mail_act_leave_allocation_second_approval'] = {'res_model': 'hr.leave.allocation', 'unlink': False}
|
||||
return info
|
||||
|
|
@ -28,7 +28,7 @@ class MailMessageSubtype(models.Model):
|
|||
department_subtype = self.create({
|
||||
'name': subtype.name,
|
||||
'res_model': 'hr.department',
|
||||
'default': subtype.default or False,
|
||||
'default': False,
|
||||
'parent_id': subtype.id,
|
||||
'relation_field': 'department_id',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
|
||||
from odoo import api, models, fields
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
leave_date_to = fields.Date(compute="_compute_leave_date_to")
|
||||
|
||||
def _compute_leave_date_to(self):
|
||||
for partner in self:
|
||||
# in the rare case of multi-user partner, return the earliest
|
||||
# possible return date
|
||||
dates = partner.user_ids.mapped("leave_date_to")
|
||||
partner.leave_date_to = min(dates) if dates and all(dates) else False
|
||||
|
||||
def _compute_im_status(self):
|
||||
super(ResPartner, self)._compute_im_status()
|
||||
super()._compute_im_status()
|
||||
absent_now = self._get_on_leave_ids()
|
||||
for partner in self:
|
||||
if partner.id in absent_now:
|
||||
|
|
@ -17,26 +25,23 @@ class ResPartner(models.Model):
|
|||
partner.im_status = 'leave_online'
|
||||
elif partner.im_status == 'away':
|
||||
partner.im_status = 'leave_away'
|
||||
else:
|
||||
elif partner.im_status == 'busy':
|
||||
partner.im_status = 'leave_busy'
|
||||
elif partner.im_status == 'offline':
|
||||
partner.im_status = 'leave_offline'
|
||||
|
||||
@api.model
|
||||
def _get_on_leave_ids(self):
|
||||
return self.env['res.users']._get_on_leave_ids(partner=True)
|
||||
|
||||
def mail_partner_format(self, fields=None):
|
||||
"""Override to add the current leave status."""
|
||||
partners_format = super().mail_partner_format(fields=fields)
|
||||
if not fields:
|
||||
fields = {'out_of_office_date_end': True}
|
||||
for partner in self:
|
||||
if 'out_of_office_date_end' in fields:
|
||||
# in the rare case of multi-user partner, return the earliest possible return date
|
||||
dates = partner.mapped('user_ids.leave_date_to')
|
||||
states = partner.mapped('user_ids.current_leave_state')
|
||||
date = sorted(dates)[0] if dates and all(dates) else False
|
||||
state = sorted(states)[0] if states and all(states) else False
|
||||
partners_format.get(partner).update({
|
||||
'out_of_office_date_end': date.strftime(DEFAULT_SERVER_DATE_FORMAT) if state == 'validate' and date else False,
|
||||
})
|
||||
return partners_format
|
||||
def _to_store_defaults(self, target):
|
||||
defaults = super()._to_store_defaults(target)
|
||||
if target.is_internal(self.env):
|
||||
# sudo: res.users - to access other company's portal user leave date
|
||||
defaults.append(
|
||||
Store.One(
|
||||
"main_user_id",
|
||||
[Store.Many("employee_ids", "leave_date_to", sudo=True), "partner_id"],
|
||||
),
|
||||
)
|
||||
return defaults
|
||||
|
|
|
|||
|
|
@ -1,38 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, Command
|
||||
from odoo import api, fields, models, Command, _
|
||||
from odoo.tools import format_date
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
leave_manager_id = fields.Many2one(related='employee_id.leave_manager_id')
|
||||
show_leaves = fields.Boolean(related='employee_id.show_leaves')
|
||||
allocation_count = fields.Float(related='employee_id.allocation_count')
|
||||
leave_date_to = fields.Date(related='employee_id.leave_date_to')
|
||||
current_leave_state = fields.Selection(related='employee_id.current_leave_state')
|
||||
is_absent = fields.Boolean(related='employee_id.is_absent')
|
||||
allocation_remaining_display = fields.Char(related='employee_id.allocation_remaining_display')
|
||||
allocation_display = fields.Char(related='employee_id.allocation_display')
|
||||
hr_icon_display = fields.Selection(related='employee_id.hr_icon_display')
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + [
|
||||
'leave_manager_id',
|
||||
'show_leaves',
|
||||
'allocation_count',
|
||||
'leave_date_to',
|
||||
'current_leave_state',
|
||||
'is_absent',
|
||||
'allocation_remaining_display',
|
||||
'allocation_display',
|
||||
'hr_icon_display',
|
||||
]
|
||||
|
||||
def _compute_im_status(self):
|
||||
super(User, self)._compute_im_status()
|
||||
super()._compute_im_status()
|
||||
on_leave_user_ids = self._get_on_leave_ids()
|
||||
for user in self:
|
||||
if user.id in on_leave_user_ids:
|
||||
|
|
@ -40,7 +25,9 @@ class User(models.Model):
|
|||
user.im_status = 'leave_online'
|
||||
elif user.im_status == 'away':
|
||||
user.im_status = 'leave_away'
|
||||
else:
|
||||
elif user.im_status == 'busy':
|
||||
user.im_status = 'leave_busy'
|
||||
elif user.im_status == 'offline':
|
||||
user.im_status = 'leave_offline'
|
||||
|
||||
@api.model
|
||||
|
|
@ -52,9 +39,10 @@ class User(models.Model):
|
|||
self.env.cr.execute('''SELECT res_users.%s FROM res_users
|
||||
JOIN hr_leave ON hr_leave.user_id = res_users.id
|
||||
AND hr_leave.state = 'validate'
|
||||
AND hr_leave.active = 't'
|
||||
AND res_users.active = 't'
|
||||
AND hr_leave.date_from <= %%s AND hr_leave.date_to >= %%s''' % field, (now, now))
|
||||
AND hr_leave.date_from <= %%s AND hr_leave.date_to >= %%s
|
||||
RIGHT JOIN hr_leave_type ON hr_leave.holiday_status_id = hr_leave_type.id
|
||||
AND hr_leave_type.time_type = 'leave';''' % field, (now, now))
|
||||
return [r[0] for r in self.env.cr.fetchall()]
|
||||
|
||||
def _clean_leave_responsible_users(self):
|
||||
|
|
@ -66,14 +54,13 @@ class User(models.Model):
|
|||
if not any(u.has_group(approver_group) for u in self):
|
||||
return
|
||||
|
||||
res = self.env['hr.employee'].read_group(
|
||||
res = self.env['hr.employee']._read_group(
|
||||
[('leave_manager_id', 'in', self.ids)],
|
||||
['leave_manager_id'],
|
||||
['leave_manager_id'])
|
||||
responsibles_to_remove_ids = set(self.ids) - {x['leave_manager_id'][0] for x in res}
|
||||
responsibles_to_remove_ids = set(self.ids) - {leave_manager.id for [leave_manager] in res}
|
||||
if responsibles_to_remove_ids:
|
||||
self.browse(responsibles_to_remove_ids).write({
|
||||
'groups_id': [Command.unlink(self.env.ref(approver_group).id)],
|
||||
'group_ids': [Command.unlink(self.env.ref(approver_group).id)],
|
||||
})
|
||||
|
||||
@api.model_create_multi
|
||||
|
|
@ -81,3 +68,12 @@ class User(models.Model):
|
|||
users = super().create(vals_list)
|
||||
users.sudo()._clean_leave_responsible_users()
|
||||
return users
|
||||
|
||||
@api.depends('leave_date_to')
|
||||
@api.depends_context('formatted_display_name')
|
||||
def _compute_display_name(self):
|
||||
super()._compute_display_name()
|
||||
for user in self:
|
||||
if user.env.context.get("formatted_display_name") and user.leave_date_to:
|
||||
name = "%s \t ✈ --%s %s--" % (user.display_name or user.name, _("Back on"), format_date(self.env, user.leave_date_to, self.env.user.lang, "medium"))
|
||||
user.display_name = name.strip()
|
||||
|
|
|
|||
|
|
@ -2,14 +2,19 @@
|
|||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools import babel_locale_parse
|
||||
from odoo.tools.date_utils import weeknumber
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
from datetime import datetime, time
|
||||
|
||||
class CalendarLeaves(models.Model):
|
||||
|
||||
class ResourceCalendarLeaves(models.Model):
|
||||
_inherit = "resource.calendar.leaves"
|
||||
|
||||
holiday_id = fields.Many2one("hr.leave", string='Leave Request')
|
||||
holiday_id = fields.Many2one("hr.leave", string='Time Off Request')
|
||||
elligible_for_accrual_rate = fields.Boolean(string='Eligible for Accrual Rate', default=False,
|
||||
help="If checked, this time off type will be taken into account for accruals computation.")
|
||||
|
||||
@api.constrains('date_from', 'date_to', 'calendar_id')
|
||||
def _check_compare_dates(self):
|
||||
|
|
@ -32,14 +37,14 @@ class CalendarLeaves(models.Model):
|
|||
raise ValidationError(_('Two public holidays cannot overlap each other for the same working hours.'))
|
||||
|
||||
def _get_domain(self, time_domain_dict):
|
||||
domain = []
|
||||
for date in time_domain_dict:
|
||||
domain = expression.OR([domain, [
|
||||
('employee_company_id', '=', date['company_id']),
|
||||
('date_to', '>', date['date_from']),
|
||||
('date_from', '<', date['date_to'])]
|
||||
])
|
||||
return expression.AND([domain, [('state', '!=', 'refuse'), ('active', '=', True)]])
|
||||
return Domain.OR(
|
||||
[
|
||||
('employee_company_id', '=', date['company_id']),
|
||||
('date_to', '>', date['date_from']),
|
||||
('date_from', '<', date['date_to']),
|
||||
]
|
||||
for date in time_domain_dict
|
||||
) & Domain('state', 'not in', ['refuse', 'cancel'])
|
||||
|
||||
def _get_time_domain_dict(self):
|
||||
return [{
|
||||
|
|
@ -59,33 +64,33 @@ class CalendarLeaves(models.Model):
|
|||
|
||||
previous_durations = leaves.mapped('number_of_days')
|
||||
previous_states = leaves.mapped('state')
|
||||
leaves.sudo().write({
|
||||
'state': 'draft',
|
||||
})
|
||||
self.env.add_to_compute(self.env['hr.leave']._fields['number_of_days'], leaves)
|
||||
self.env.add_to_compute(self.env['hr.leave']._fields['duration_display'], leaves)
|
||||
sick_time_status = self.env.ref('hr_holidays.holiday_status_sl')
|
||||
leaves.sudo().write({
|
||||
'state': 'confirm',
|
||||
})
|
||||
sick_time_status = self.env.ref('hr_holidays.leave_type_sick_time_off', raise_if_not_found=False)
|
||||
leaves_to_recreate = self.env['hr.leave']
|
||||
for previous_duration, leave, state in zip(previous_durations, leaves, previous_states):
|
||||
duration_difference = previous_duration - leave.number_of_days
|
||||
if duration_difference > 0 and leave['holiday_allocation_id'] and leave.number_of_days == 0.0:
|
||||
message = False
|
||||
if duration_difference > 0 and leave.holiday_status_id.requires_allocation:
|
||||
message = _("Due to a change in global time offs, you have been granted %s day(s) back.", duration_difference)
|
||||
leave._notify_change(message)
|
||||
if leave.number_of_days > previous_duration\
|
||||
and leave.holiday_status_id not in sick_time_status:
|
||||
new_leaves = leave.split_leave(time_domain_dict)
|
||||
leaves |= new_leaves
|
||||
previous_states += [state] * len(new_leaves)
|
||||
|
||||
leaves_to_cancel = self.env['hr.leave']
|
||||
for state, leave in zip(previous_states, leaves):
|
||||
leave.write({'state': state})
|
||||
if leave.number_of_days == 0.0:
|
||||
leaves_to_cancel |= leave
|
||||
elif leave.state == 'validate':
|
||||
# recreate the resource leave that were removed by writing state to draft
|
||||
leave.sudo()._create_resource_leave()
|
||||
|
||||
leaves_to_cancel._force_cancel(_("a new public holiday completely overrides this leave."), 'mail.mt_comment')
|
||||
and (not sick_time_status or leave.holiday_status_id not in sick_time_status):
|
||||
message = _("Due to a change in global time offs, %s extra day(s) have been taken from your allocation. Please review this leave if you need it to be changed.", -1 * duration_difference)
|
||||
try:
|
||||
leave.sudo().write({'state': state}) # sudo in order to skip _check_approval_update
|
||||
leave._check_validity()
|
||||
if leave.state == 'validate':
|
||||
# recreate the resource leave that were removed by writing state to draft
|
||||
leaves_to_recreate |= leave
|
||||
except ValidationError:
|
||||
leave.action_refuse()
|
||||
message = _("Due to a change in global time offs, this leave no longer has the required amount of available allocation and has been set to refused. Please review this leave.")
|
||||
if message:
|
||||
leave._notify_change(message)
|
||||
leaves_to_recreate.sudo()._create_resource_leave()
|
||||
|
||||
def _convert_timezone(self, utc_naive_datetime, tz_from, tz_to):
|
||||
"""
|
||||
|
|
@ -157,18 +162,65 @@ class CalendarLeaves(models.Model):
|
|||
|
||||
return res
|
||||
|
||||
@api.depends('calendar_id')
|
||||
def _compute_company_id(self):
|
||||
for leave in self:
|
||||
leave.company_id = leave.holiday_id.employee_id.company_id or leave.calendar_id.company_id or self.env.company
|
||||
|
||||
|
||||
class ResourceCalendar(models.Model):
|
||||
_inherit = "resource.calendar"
|
||||
|
||||
associated_leaves_count = fields.Integer("Leave Count", compute='_compute_associated_leaves_count')
|
||||
associated_leaves_count = fields.Integer("Time Off Count", compute='_compute_associated_leaves_count')
|
||||
|
||||
def _compute_associated_leaves_count(self):
|
||||
leaves_read_group = self.env['resource.calendar.leaves'].read_group(
|
||||
[('resource_id', '=', False)],
|
||||
leaves_read_group = self.env['resource.calendar.leaves']._read_group(
|
||||
[('resource_id', '=', False), ('calendar_id', 'in', self.ids)],
|
||||
['calendar_id'],
|
||||
['calendar_id']
|
||||
['__count'],
|
||||
)
|
||||
result = dict((data['calendar_id'][0] if data['calendar_id'] else 'global', data['calendar_id_count']) for data in leaves_read_group)
|
||||
result = {calendar.id if calendar else 'global': count for calendar, count in leaves_read_group}
|
||||
global_leave_count = result.get('global', 0)
|
||||
for calendar in self:
|
||||
calendar.associated_leaves_count = result.get(calendar.id, 0) + global_leave_count
|
||||
|
||||
|
||||
class ResourceResource(models.Model):
|
||||
_inherit = "resource.resource"
|
||||
|
||||
leave_date_to = fields.Date(related="user_id.leave_date_to")
|
||||
|
||||
def _format_leave(self, leave, resource_hours_per_day, resource_hours_per_week, ranges_to_remove, start_day, end_day, locale):
|
||||
leave_start = leave[0]
|
||||
leave_record = leave[2]
|
||||
holiday_id = leave_record.holiday_id
|
||||
tz = pytz.timezone(self.tz or self.env.user.tz)
|
||||
|
||||
if holiday_id.request_unit_half:
|
||||
# Half day leaves are limited to half a day within a single day
|
||||
leave_day = leave_start.date()
|
||||
half_start_datetime = tz.localize(datetime.combine(leave_day, datetime.min.time() if holiday_id.request_date_from_period == "am" else time(12)))
|
||||
half_end_datetime = tz.localize(datetime.combine(leave_day, time(12) if holiday_id.request_date_from_period == "am" else datetime.max.time()))
|
||||
ranges_to_remove.append((half_start_datetime, half_end_datetime, self.env['resource.calendar.attendance']))
|
||||
|
||||
if not self._is_fully_flexible():
|
||||
# only days inside the original period
|
||||
if leave_day >= start_day and leave_day <= end_day:
|
||||
resource_hours_per_day[self.id][leave_day] -= holiday_id.number_of_hours
|
||||
week = weeknumber(babel_locale_parse(locale), leave_day)
|
||||
resource_hours_per_week[self.id][week] -= holiday_id.number_of_hours
|
||||
elif holiday_id.request_unit_hours:
|
||||
# Custom leaves are limited to a specific number of hours within a single day
|
||||
leave_day = leave_start.date()
|
||||
range_start_datetime = pytz.utc.localize(leave_record.date_from).replace(tzinfo=None).astimezone(tz)
|
||||
range_end_datetime = pytz.utc.localize(leave_record.date_to).replace(tzinfo=None).astimezone(tz)
|
||||
ranges_to_remove.append((range_start_datetime, range_end_datetime, self.env['resource.calendar.attendance']))
|
||||
|
||||
if not self._is_fully_flexible():
|
||||
# only days inside the original period
|
||||
if leave_day >= start_day and leave_day <= end_day:
|
||||
resource_hours_per_day[self.id][leave_day] -= holiday_id.number_of_hours
|
||||
week = weeknumber(babel_locale_parse(locale), leave_day)
|
||||
resource_hours_per_week[self.id][week] -= holiday_id.number_of_hours
|
||||
else:
|
||||
super()._format_leave(leave, resource_hours_per_day, resource_hours_per_week, ranges_to_remove, start_day, end_day, locale)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue