forked from Mapan/odoo17e
209 lines
9.5 KiB
Python
209 lines
9.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
|
|
|
|
from collections import defaultdict
|
|
from datetime import timedelta
|
|
from itertools import groupby
|
|
from pytz import timezone, utc
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.tools.misc import get_lang
|
|
|
|
|
|
def format_time(env, time):
|
|
return time.strftime(get_lang(env).time_format)
|
|
|
|
|
|
def format_date(env, date):
|
|
return date.strftime(get_lang(env).date_format)
|
|
|
|
|
|
class HrLeave(models.Model):
|
|
_inherit = "hr.leave"
|
|
|
|
@api.model
|
|
def _get_leave_interval(self, date_from, date_to, employee_ids):
|
|
# Validated hr.leave create a resource.calendar.leaves
|
|
calendar_leaves = self.env['resource.calendar.leaves'].search([
|
|
('time_type', '=', 'leave'),
|
|
'|', ('company_id', 'in', employee_ids.mapped('company_id').ids),
|
|
('company_id', '=', False),
|
|
'|', ('resource_id', 'in', employee_ids.mapped('resource_id').ids),
|
|
('resource_id', '=', False),
|
|
('date_from', '<=', date_to),
|
|
('date_to', '>=', date_from),
|
|
], order='date_from')
|
|
|
|
leaves = defaultdict(list)
|
|
for leave in calendar_leaves:
|
|
for employee in employee_ids:
|
|
if (not leave.company_id or leave.company_id == employee.company_id) and\
|
|
(not leave.resource_id or leave.resource_id == employee.resource_id) and\
|
|
(not leave.calendar_id or leave.calendar_id == employee.resource_calendar_id):
|
|
leaves[employee.id].append(leave)
|
|
|
|
# Get non-validated time off
|
|
leaves_query = self.env['hr.leave'].search([
|
|
('employee_id', 'in', employee_ids.ids),
|
|
('state', 'in', ['confirm', 'validate1']),
|
|
('date_from', '<=', date_to),
|
|
('date_to', '>=', date_from)
|
|
], order='date_from')
|
|
for leave in leaves_query:
|
|
leaves[leave.employee_id.id].append(leave)
|
|
return leaves
|
|
|
|
def _get_leave_warning_parameters(self, leaves, employee, date_from, date_to):
|
|
loc_cache = {}
|
|
|
|
def localize(date):
|
|
if date not in loc_cache:
|
|
loc_cache[date] = utc.localize(date).astimezone(timezone(self.env.user.tz or 'UTC')).replace(tzinfo=None)
|
|
return loc_cache[date]
|
|
|
|
periods = self._group_leaves(leaves, employee, date_from, date_to)
|
|
periods_by_states = [list(b) for a, b in groupby(periods, key=lambda x: x['is_validated'])]
|
|
res = {}
|
|
for periods in periods_by_states:
|
|
leaves_for_employee = {'name': employee.name, "leaves": []}
|
|
for period in periods:
|
|
dfrom = period['from']
|
|
dto = period['to']
|
|
if period.get('show_hours', False):
|
|
leaves_for_employee['leaves'].append({
|
|
"start_date": format_date(self.env, localize(dfrom)),
|
|
"start_time": format_time(self.env, localize(dfrom)),
|
|
"end_date": format_date(self.env, localize(dto)),
|
|
"end_time": format_time(self.env, localize(dto))
|
|
})
|
|
else:
|
|
leaves_for_employee['leaves'].append({
|
|
"start_date": format_date(self.env, localize(dfrom)),
|
|
"end_date": format_date(self.env, localize(dto)),
|
|
})
|
|
res["validated" if periods[0].get('is_validated') else "requested"] = leaves_for_employee
|
|
return res
|
|
|
|
def format_date_range_to_string(self, date_dict):
|
|
if len(date_dict) == 4:
|
|
return _('from %(start_date)s at %(start_time)s to %(end_date)s at %(end_time)s', **date_dict)
|
|
else:
|
|
return _('from %(start_date)s to %(end_date)s', **date_dict)
|
|
|
|
def _get_leave_warning(self, leaves, employee, date_from, date_to):
|
|
leaves_parameters = self._get_leave_warning_parameters(leaves, employee, date_from, date_to)
|
|
warning = ''
|
|
for leave_type, leaves_for_employee in leaves_parameters.items():
|
|
if not leaves_for_employee:
|
|
continue
|
|
if leave_type == "validated":
|
|
warning += _(
|
|
'%(name)s is on time off %(leaves)s. \n',
|
|
name=leaves_for_employee["name"],
|
|
leaves=', '.join(map(self.format_date_range_to_string, leaves_for_employee["leaves"]))
|
|
)
|
|
else:
|
|
warning += _(
|
|
'%(name)s requested time off %(leaves)s. \n',
|
|
name=leaves_for_employee["name"],
|
|
leaves=', '.join(map(self.format_date_range_to_string, leaves_for_employee["leaves"]))
|
|
)
|
|
return warning
|
|
|
|
def _group_leaves(self, leaves, employee_id, date_from, date_to):
|
|
"""
|
|
Returns all the leaves happening between `planned_date_begin` and `date_deadline`
|
|
"""
|
|
work_times = {wk[0]: wk[1] for wk in employee_id.list_work_time_per_day(date_from, date_to)}
|
|
|
|
def has_working_hours(start_dt, end_dt):
|
|
"""
|
|
Returns `True` if there are any working days between `start_dt` and `end_dt`.
|
|
"""
|
|
diff_days = (end_dt - start_dt).days
|
|
all_dates = [start_dt.date() + timedelta(days=delta) for delta in range(diff_days + 1)]
|
|
return any(d in work_times for d in all_dates)
|
|
|
|
periods = []
|
|
for leave in leaves:
|
|
if leave.date_from > date_to or leave.date_to < date_from:
|
|
continue
|
|
|
|
# Can handle both hr.leave and resource.calendar.leaves
|
|
number_of_days = 0
|
|
is_validated = True
|
|
if isinstance(leave, self.pool['hr.leave']):
|
|
number_of_days = leave.number_of_days
|
|
is_validated = False
|
|
else:
|
|
dt_delta = (leave.date_to - leave.date_from)
|
|
number_of_days = dt_delta.days + ((dt_delta.seconds / 3600) / 24)
|
|
# leaves are ordered by date_from and grouped by type. When go from the batch of validated time offs to the
|
|
# requested ones, we need to bypass the second condition with the third one
|
|
if not periods or has_working_hours(periods[-1]['from'], leave.date_to) or \
|
|
periods[-1]['is_validated'] != is_validated:
|
|
periods.append({'is_validated': is_validated, 'from': leave.date_from, 'to': leave.date_to, 'show_hours': number_of_days <= 1})
|
|
else:
|
|
periods[-1]['is_validated'] = is_validated
|
|
if periods[-1]['to'] < leave.date_to:
|
|
periods[-1]['to'] = leave.date_to
|
|
periods[-1]['show_hours'] = periods[-1].get('show_hours') or number_of_days <= 1
|
|
return periods
|
|
|
|
@api.model
|
|
def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None):
|
|
start_datetime = fields.Datetime.from_string(start_date)
|
|
end_datetime = fields.Datetime.from_string(end_date)
|
|
employee_ids = tag_employee_rows(rows)
|
|
employees = self.env['hr.employee'].browse(employee_ids)
|
|
leaves_mapping = employees.mapped('resource_id')._get_unavailable_intervals(start_datetime, end_datetime)
|
|
|
|
cell_dt = timedelta(hours=1) if scale in ['day', 'week'] else timedelta(hours=12)
|
|
|
|
# for a single row, inject unavailability data
|
|
def inject_unvailabilty(row):
|
|
new_row = dict(row)
|
|
|
|
if row.get('employee_id'):
|
|
employee_id = self.env['hr.employee'].browse(row.get('employee_id'))
|
|
if employee_id:
|
|
# remove intervals smaller than a cell, as they will cause half a cell to turn grey
|
|
# ie: when looking at a week, a employee start everyday at 8, so there is a unavailability
|
|
# like: 2019-05-22 20:00 -> 2019-05-23 08:00 which will make the first half of the 23's cell grey
|
|
notable_intervals = filter(lambda interval: interval[1] - interval[0] >= cell_dt, leaves_mapping[employee_id.resource_id.id])
|
|
new_row['unavailabilities'] = [{'start': interval[0], 'stop': interval[1]} for interval in notable_intervals]
|
|
return new_row
|
|
|
|
return [traverse(inject_unvailabilty, row) for row in rows]
|
|
|
|
def tag_employee_rows(rows):
|
|
"""
|
|
Add `employee_id` key in rows and subsrows recursively if necessary
|
|
:return: a set of ids with all concerned employees (subrows included)
|
|
"""
|
|
employee_ids = set()
|
|
for row in rows:
|
|
group_bys = row.get('groupedBy')
|
|
res_id = row.get('resId')
|
|
if group_bys:
|
|
# if employee_id is the first grouping attribute, we mark the row
|
|
if group_bys[0] == 'employee_id' and res_id:
|
|
employee_id = res_id
|
|
employee_ids.add(employee_id)
|
|
row['employee_id'] = employee_id
|
|
# else we recursively traverse the rows where employee_id appears in the group_by
|
|
elif 'employee_id' in group_bys:
|
|
employee_ids.update(tag_employee_rows(row.get('rows')))
|
|
return employee_ids
|
|
|
|
# function to recursively replace subrows with the ones returned by func
|
|
def traverse(func, row):
|
|
new_row = dict(row)
|
|
if new_row.get('employee_id'):
|
|
for sub_row in new_row.get('rows'):
|
|
sub_row['employee_id'] = new_row['employee_id']
|
|
new_row['rows'] = [traverse(func, row) for row in new_row.get('rows')]
|
|
return func(new_row)
|