305 lines
12 KiB
Python
305 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import pytz
|
|
import itertools
|
|
from psycopg2 import IntegrityError
|
|
from dateutil.relativedelta import relativedelta
|
|
from odoo import api, fields, models, exceptions, _
|
|
from odoo.tools import mute_logger
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.addons.resource.models.resource import Intervals
|
|
|
|
class HrBenefit(models.Model):
|
|
_name = 'hr.benefit'
|
|
_description = 'hr.benefit'
|
|
_order = 'display_warning desc,state,date_start'
|
|
|
|
name = fields.Char(required=True)
|
|
active = fields.Boolean(default=True)
|
|
employee_id = fields.Many2one('hr.employee', required=True,
|
|
domain=lambda self: [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)])
|
|
date_start = fields.Datetime(required=True, string='From')
|
|
date_stop = fields.Datetime(string='To')
|
|
duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Period")
|
|
contract_id = fields.Many2one('hr.contract', string="Contract", required=True)
|
|
benefit_type_id = fields.Many2one('hr.benefit.type')
|
|
color = fields.Integer(related='benefit_type_id.color', readonly=True)
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('confirmed', 'Confirmed'),
|
|
('validated', 'Validated'),
|
|
('cancelled', 'Cancelled')
|
|
], default='draft')
|
|
display_warning = fields.Boolean(string="Error")
|
|
leave_id = fields.Many2one('hr.leave', string='Leave')
|
|
company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True,
|
|
default=lambda self: self.env['res.company']._company_default_get())
|
|
|
|
_sql_constraints = [
|
|
('_unique', 'unique (employee_id, date_start, date_stop, benefit_type_id)', "Benefit already exists for this attendence"),
|
|
]
|
|
|
|
@api.constrains('date_stop', 'duration')
|
|
def _check_validity_benefit_ends(self):
|
|
""" verifies if benefit has an end. """
|
|
for benefit in self:
|
|
if not (benefit.date_stop or benefit.duration):
|
|
raise exceptions.ValidationError(_('Benefit must end. Please define an end date or a duration.'))
|
|
|
|
@api.constrains('date_start', 'date_stop')
|
|
def _check_validity_start_before_end(self):
|
|
""" verifies if benefit has an end. """
|
|
for benefit in self:
|
|
if benefit.date_stop < benefit.date_start:
|
|
raise exceptions.ValidationError(_('Starting time should be before end time.'))
|
|
|
|
|
|
@api.onchange('duration')
|
|
def _onchange_duration(self):
|
|
self._inverse_duration()
|
|
|
|
@api.depends('date_stop', 'date_start')
|
|
def _compute_duration(self):
|
|
for benefit in self:
|
|
if benefit.date_start and benefit.date_stop:
|
|
dt = benefit.date_stop - benefit.date_start
|
|
benefit.duration = dt.days * 24 + dt.seconds / 3600 # Number of hours
|
|
|
|
def _inverse_duration(self):
|
|
for benefit in self:
|
|
if benefit.date_start and benefit.duration:
|
|
benefit.date_stop = benefit.date_start + relativedelta(hours=benefit.duration)
|
|
|
|
def write(self, vals):
|
|
if 'state' in vals:
|
|
if vals['state'] == 'draft':
|
|
vals['active'] = True
|
|
if vals['state'] == 'cancelled':
|
|
vals['active'] = False
|
|
self.mapped('leave_id').action_refuse()
|
|
return super(HrBenefit, self).write(vals)
|
|
|
|
@api.multi
|
|
def check_if_error(self):
|
|
if not self:
|
|
return False
|
|
undefined_type = self.filtered(lambda b: not b.benefit_type_id)
|
|
undefined_type.write({'display_warning': True})
|
|
conflict = self._compute_schedule_conflicts()
|
|
conflict_with_leaves = self.compute_conflicts_leaves_to_approve()
|
|
return undefined_type or conflict or conflict_with_leaves
|
|
|
|
@api.multi
|
|
def _compute_schedule_conflicts(self):
|
|
conflict = False
|
|
date_start_benefits = min(self.mapped('date_start'))
|
|
date_stop_benefits = max(self.mapped('date_stop'))
|
|
domain = [
|
|
('date_start', '<', date_stop_benefits),
|
|
('date_stop', '>', date_start_benefits),
|
|
]
|
|
|
|
benefs = self.search(domain)
|
|
benefits_by_employee = itertools.groupby(benefs, lambda b: b.employee_id)
|
|
for employee, benefs in benefits_by_employee:
|
|
intervals = Intervals(intervals=((b.date_start, b.date_stop, b) for b in benefs))
|
|
for interval in intervals:
|
|
if len(interval[2]) > 1:
|
|
interval[2].write({'display_warning': True})
|
|
conflict = True
|
|
return conflict
|
|
|
|
@api.multi
|
|
def compute_conflicts_leaves_to_approve(self):
|
|
|
|
if not self:
|
|
return False
|
|
|
|
query = """
|
|
SELECT
|
|
b.id AS benefit_id,
|
|
l.id AS leave_id
|
|
FROM hr_benefit b
|
|
INNER JOIN hr_leave l ON b.employee_id = l.employee_id
|
|
WHERE
|
|
b.id IN %s AND
|
|
l.date_from <= b.date_stop AND
|
|
l.date_to >= b.date_start AND
|
|
l.state IN ('confirm', 'validate1');
|
|
"""
|
|
self.env.cr.execute(query, [tuple(self.ids)])
|
|
conflicts = self.env.cr.dictfetchall()
|
|
for res in conflicts:
|
|
self.browse(res.get('benefit_id')).write({
|
|
'display_warning': True,
|
|
'leave_id': res.get('leave_id')
|
|
})
|
|
return bool(conflicts)
|
|
|
|
def safe_duplicate_create(self, vals):
|
|
"""
|
|
Create benefit but silently abord if it already exists in the database (according to the _unique constraints).
|
|
@return: new record if it didn't exist, empty recordset otherwise.
|
|
"""
|
|
try:
|
|
with mute_logger('odoo.sql_db'), self.env.cr.savepoint():
|
|
# Any database call will be run inside a postgresql savepoint which is
|
|
# rolledback if an exception occurs allowing to make subsequent calls in the transaction.
|
|
# (Otherwise InternalError is raised by any subsequent database operations)
|
|
return self.create(vals)
|
|
except IntegrityError as err:
|
|
if 'hr_benefit__unique' not in err.pgerror:
|
|
raise err
|
|
return self.env['hr.benefit']
|
|
|
|
def action_leave(self):
|
|
leave = self.leave_id
|
|
return {
|
|
'type':'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_id': leave.id,
|
|
'res_model': 'hr.leave',
|
|
'views': [[False, 'form']],
|
|
}
|
|
|
|
def split_by_day(self):
|
|
"""
|
|
Split the benefit by days and unlink the original benefit.
|
|
@return recordset
|
|
"""
|
|
def _split_range_by_day(start, end):
|
|
days = []
|
|
current_start = start
|
|
current_end = start.replace(hour=23, minute=59, second=59)
|
|
while current_end < end:
|
|
days.append((current_start, current_end))
|
|
current_start = current_end + relativedelta(seconds=1)
|
|
current_end = current_end + relativedelta(days=1)
|
|
|
|
days.append((current_start, end))
|
|
|
|
# filter to avoid dummy intervals starting and ending at the same time
|
|
return [(start, end) for start, end in days if start != end]
|
|
|
|
new_benefits = self.env['hr.benefit']
|
|
for benefit in self:
|
|
if benefit.date_start.date() == benefit.date_stop.date():
|
|
new_benefits |= benefit
|
|
else:
|
|
tz = pytz.timezone(benefit.employee_id.tz)
|
|
benefit_start, benefit_stop = tz.localize(benefit.date_start), tz.localize(benefit.date_stop)
|
|
values = {
|
|
'name': benefit.name,
|
|
'employee_id': benefit.employee_id.id,
|
|
'benefit_type_id': benefit.benefit_type_id.id,
|
|
'contract_id': benefit.contract_id.id,
|
|
}
|
|
benefit_state = benefit.state
|
|
benefit.unlink()
|
|
for start, stop in _split_range_by_day(benefit_start, benefit_stop):
|
|
values['date_start'] = start.astimezone(pytz.utc)
|
|
values['date_stop'] = stop.astimezone(pytz.utc)
|
|
new_benefit = self.create(values)
|
|
# Write the state after the creation due to the ir.rule on benefit state
|
|
new_benefit.state = benefit_state
|
|
new_benefits |= new_benefit
|
|
|
|
return new_benefits
|
|
|
|
@api.multi
|
|
def _duplicate_to_calendar(self):
|
|
"""
|
|
Duplicate data to keep the complexity in benefit and not mess up payroll, etc.
|
|
"""
|
|
attendance_benefits = self.filtered(lambda b:
|
|
not b.benefit_type_id.is_leave and
|
|
# Normal benefit are global to all employees -> avoid duplicating it
|
|
not b.benefit_type_id == self.env.ref('hr_payroll.benefit_type_attendance'))
|
|
leave_benefits = self.filtered(lambda b: b.benefit_type_id.is_leave)
|
|
|
|
for benefit in attendance_benefits:
|
|
benefit = benefit.split_by_day()
|
|
benefit._duplicate_to_calendar_attendance()
|
|
leave_benefits._duplicate_to_calendar_leave()
|
|
|
|
@api.multi
|
|
def _duplicate_to_calendar_leave(self):
|
|
|
|
for benefit in self:
|
|
if not benefit.leave_id:
|
|
tz = pytz.timezone(benefit.employee_id.tz)
|
|
self.env['resource.calendar.leaves'].create({
|
|
'name': benefit.name,
|
|
'date_from': benefit.date_start,
|
|
'date_to': benefit.date_stop,
|
|
'calendar_id': benefit.employee_id.resource_calendar_id.id,
|
|
'resource_id': benefit.employee_id.resource_id.id,
|
|
'benefit_type_id': benefit.benefit_type_id.id,
|
|
})
|
|
|
|
@api.multi
|
|
def _duplicate_to_calendar_attendance(self):
|
|
mapped_data = {
|
|
benefit: [
|
|
pytz.utc.localize(benefit.date_start).astimezone(pytz.timezone(benefit.employee_id.tz)), # Start date
|
|
pytz.utc.localize(benefit.date_stop).astimezone(pytz.timezone(benefit.employee_id.tz)) # End date
|
|
] for benefit in self
|
|
}
|
|
|
|
if any(data[0].date() != data[1].date() for data in mapped_data.values()):
|
|
raise ValidationError(_("You can't validate a benefit that covers several days."))
|
|
|
|
for benefit in self:
|
|
start, end = mapped_data.get(benefit)
|
|
|
|
self.env['resource.calendar.attendance'].create({
|
|
'name': benefit.name,
|
|
'dayofweek': str(start.weekday()),
|
|
'date_from': start.date(),
|
|
'date_to': end.date(),
|
|
'hour_from':start.hour + start.minute/60,
|
|
'hour_to': end.hour + end.minute/60,
|
|
'calendar_id': benefit.contract_id.resource_calendar_id.id,
|
|
'day_period': 'morning' if end.hour <= 12 else 'afternoon',
|
|
'resource_id': benefit.employee_id.resource_id.id,
|
|
'benefit_type_id': benefit.benefit_type_id.id,
|
|
})
|
|
|
|
@api.model
|
|
def action_validate(self, ids):
|
|
benefits = self.env['hr.benefit'].search([('id', 'in', ids), ('state', '!=', 'validated')])
|
|
benefits.write({'display_warning': False})
|
|
if not benefits.check_if_error():
|
|
benefits.write({'state': 'validated'})
|
|
benefits._duplicate_to_calendar()
|
|
return True
|
|
return False
|
|
|
|
class HrBenefitType(models.Model):
|
|
_name = 'hr.benefit.type'
|
|
_description = 'hr.benefit.type'
|
|
|
|
name = fields.Char(required=True)
|
|
code = fields.Char()
|
|
color = fields.Integer(default=1) # Will be used with the new calendar/kanban view
|
|
sequence = fields.Integer(default=25)
|
|
active = fields.Boolean('Active', default=True,
|
|
help="If the active field is set to false, it will allow you to hide the benefit type without removing it.")
|
|
is_leave = fields.Boolean(default=False, string="Leave")
|
|
|
|
class Contacts(models.Model):
|
|
""" Personnal calendar filter """
|
|
|
|
_name = 'hr.user.benefit.employee'
|
|
_description = 'Benefits Employees'
|
|
|
|
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user)
|
|
employee_id = fields.Many2one('hr.employee', 'Employee', required=True)
|
|
active = fields.Boolean('Active', default=True)
|
|
|
|
_sql_constraints = [
|
|
('user_id_employee_id_unique', 'UNIQUE(user_id,employee_id)', 'You cannot have twice the same employee.')
|
|
]
|