# -*- encoding: utf-8 -*- ############################################################################## # # HR Holidays Usability module for Odoo # Copyright (C) 2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################## from openerp import models, fields, api, _ from openerp.exceptions import ValidationError from datetime import datetime from dateutil.relativedelta import relativedelta import pytz import logging logger = logging.getLogger(__name__) class HrHolidaysStatus(models.Model): _inherit = 'hr.holidays.status' vacation_compute_method = fields.Selection([ ('worked', u'Jours ouvrés'), ('business', u'Jours ouvrables'), # TODO find proper English translation ], string='Vacation Compute Method', required=True, default='worked') class HrHolidays(models.Model): _inherit = 'hr.holidays' _order = 'type desc, date_from desc' # by default : type desc, date_from asc # Idea : # For allocation (type = add), we don't change anything: # The user writes in the field number_of_days_temp and # number_of_days = number_of_days_temp # IN 7.0: For leave (type = remove), we don't let users enter the number # of days, we compute it for them # -> new computed field "number_of_days_remove' that compute the number # of days depending on the computation method defined on 'type' # Redefine the field 'number_of_days' to take into accout # 'number_of_days_remove' when type == remove (= number_of_days_remove * -1) # IN 8.0: for leaves, use the on_change on the dates to set the # number_of_days_temp. But we want to have this field readonly, so we also # inherit write + create (even in v8, readonly fields are not written in DB) # change date time fields by date + selection morning/noon # How do we set the dates : # from : premier jour d'absence (et non dernier jour de présence) # + time : morning/noon # to : date de fin de congés (et non date de retour au travail) # + time : noon/evening # which computation methods on the 'hr.holidays.status': # 1) jours ouvrés (sans compter les jours fériés) # 2) jours ouvrables (quid des jours fériés ???) : # il faut compter les samedis sauf les samedis fériés. # Cas particulier : quand la personne prend le vendredi aprèm, # il faut compter 1j (et non 0.5 ni 1.5) # 3) malade : on compte tous les jours -> ptet pas nécessaire pour le moment # 1 for 'unpaid leaves' + repos compensateur + congés conventionnels + maladie # 1 or 2 for normal holidays @api.model def _compute_number_of_days(self): # depend on the holiday_status_id hhpo = self.env['hr.holidays.public'] days = 0.0 if ( self.type == 'remove' and self.holiday_type == 'employee' and self.vacation_date_from and self.vacation_time_from and self.vacation_date_to and self.vacation_time_to and self.vacation_date_from <= self.vacation_date_to): if self.holiday_status_id.vacation_compute_method == 'business': business = True else: business = False date_dt = start_date_dt = fields.Date.from_string( self.vacation_date_from) end_date_dt = fields.Date.from_string( self.vacation_date_to) while True: # REGULAR COMPUTATION # if it's a bank holidays, don't count if hhpo.is_public_holiday(date_dt, self.employee_id.id): logger.info( "%s is a bank holiday, don't count", date_dt) # it it's a saturday/sunday elif date_dt.weekday() in (5, 6): logger.info( "%s is a saturday/sunday, don't count", date_dt) else: days += 1.0 # special case for friday when compute_method = business if ( business and date_dt.weekday() == 4 and not hhpo.is_public_holiday( date_dt + relativedelta(days=1), self.employee_id.id)): days += 1.0 # PARTICULAR CASE OF THE FIRST DAY if date_dt == start_date_dt: if self.vacation_time_from == 'noon': if ( business and date_dt.weekday() == 4 and not hhpo.is_public_holiday( date_dt + relativedelta(days=1), self.employee_id.id)): days -= 1.0 # result = 2 - 1 = 1 else: days -= 0.5 # PARTICULAR CASE OF THE LAST DAY if date_dt == end_date_dt: if self.vacation_time_to == 'noon': if ( business and date_dt.weekday() == 4 and not hhpo.is_public_holiday( date_dt + relativedelta(days=1), self.employee_id.id)): days -= 1.5 # 2 - 1.5 = 0.5 else: days -= 0.5 break date_dt += relativedelta(days=1) return days @api.one @api.depends('holiday_type', 'employee_id', 'holiday_status_id') def _compute_current_leaves(self): total_allocated_leaves = 0 current_leaves_taken = 0 current_remaining_leaves = 0 if ( self.holiday_type == 'employee' and self.employee_id and self.holiday_status_id): days = self.holiday_status_id.get_days(self.employee_id.id) total_allocated_leaves =\ days[self.holiday_status_id.id]['max_leaves'] current_leaves_taken =\ days[self.holiday_status_id.id]['leaves_taken'] current_remaining_leaves =\ days[self.holiday_status_id.id]['remaining_leaves'] self.total_allocated_leaves = total_allocated_leaves self.current_leaves_taken = current_leaves_taken self.current_remaining_leaves = current_remaining_leaves vacation_date_from = fields.Date( string='First Day of Vacation', track_visibility='onchange', help="Enter the first day of vacation. For example, if " "you leave one full calendar week, the first day of vacation " "is Monday morning (and not Friday of the week before)") vacation_time_from = fields.Selection([ ('morning', 'Morning'), ('noon', 'Noon'), ], string="Start of Vacation", track_visibility='onchange', default='morning', help="For example, if you leave one full calendar week, " "the first day of vacation is Monday Morning") vacation_date_to = fields.Date( string='Last Day of Vacation', track_visibility='onchange', help="Enter the last day of vacation. For example, if you " "leave one full calendar week, the last day of vacation is " "Friday evening (and not Monday of the week after)") vacation_time_to = fields.Selection([ ('noon', 'Noon'), ('evening', 'Evening'), ], string="End of Vacation", track_visibility='onchange', default='evening', help="For example, if you leave one full calendar week, " "the end of vacation is Friday Evening") current_leaves_taken = fields.Float( compute='_compute_current_leaves', string='Current Leaves Taken', readonly=True) current_remaining_leaves = fields.Float( compute='_compute_current_leaves', string='Current Remaining Leaves', readonly=True) total_allocated_leaves = fields.Float( compute='_compute_current_leaves', string='Total Allocated Leaves', readonly=True) limit = fields.Boolean( # pose des pbs de droits related='holiday_status_id.limit', string='Allow to Override Limit', readonly=True) posted_date = fields.Date( string='Posted Date', track_visibility='onchange') number_of_days_temp = fields.Float(string="Number of days") @api.one @api.constrains( 'vacation_date_from', 'vacation_date_to', 'holiday_type', 'type') def _check_vacation_dates(self): hhpo = self.env['hr.holidays.public'] if self.type == 'remove': if self.vacation_date_from > self.vacation_date_to: raise ValidationError( _('The first day cannot be after the last day !')) elif ( self.vacation_date_from == self.vacation_date_to and self.vacation_time_from == self.vacation_time_to): raise ValidationError( _("The start of vacation is exactly the " "same as the end !")) date_from_dt = fields.Date.from_string( self.vacation_date_from) if date_from_dt.weekday() in (5, 6): raise ValidationError( _("The first day of vacation cannot be a " "saturday or sunday !")) date_to_dt = fields.Date.from_string( self.vacation_date_to) if date_to_dt.weekday() in (5, 6): raise ValidationError( _("The last day of Vacation cannot be a " "saturday or sunday !")) if hhpo.is_public_holiday(date_from_dt, self.employee_id.id): raise ValidationError( _("The first day of vacation cannot be a " "bank holiday !")) if hhpo.is_public_holiday(date_to_dt, self.employee_id.id): raise ValidationError( _("The last day of vacation cannot be a " "bank holiday !")) @api.onchange('vacation_date_from', 'vacation_time_from') def vacation_from(self): hour = 0 # = morning if self.vacation_time_from and self.vacation_time_from == 'noon': hour = 13 # noon, LOCAL TIME # Warning : when the vacation STARTs at Noon, it starts at 1 p.m. # to avoid an overlap (which would be blocked by a constraint of # hr_holidays) if a user requests 2 half-days with different # holiday types on the same day datetime_str = False if self.vacation_date_from: date_dt = fields.Date.from_string(self.vacation_date_from) if self._context.get('tz'): localtz = pytz.timezone(self._context['tz']) else: localtz = pytz.utc datetime_dt = localtz.localize(datetime( date_dt.year, date_dt.month, date_dt.day, hour, 0, 0)) # we give to odoo a datetime in UTC datetime_str = fields.Datetime.to_string( datetime_dt.astimezone(pytz.utc)) self.date_from = datetime_str @api.onchange('vacation_date_to', 'vacation_time_to') def vacation_to(self): hour = 23 # = evening if self.vacation_time_to and self.vacation_time_to == 'noon': hour = 12 # Noon, LOCAL TIME # Warning : when vacation STOPs at Noon, it stops at 12 a.m. # to avoid an overlap (which would be blocked by a constraint of # hr_holidays) if a user requests 2 half-days with different # holiday types on the same day datetime_str = False if self.vacation_date_to: date_dt = fields.Date.from_string(self.vacation_date_to) if self._context.get('tz'): localtz = pytz.timezone(self._context['tz']) else: localtz = pytz.utc datetime_dt = localtz.localize(datetime( date_dt.year, date_dt.month, date_dt.day, hour, 0, 0)) # we give to odoo a datetime in UTC datetime_str = fields.Datetime.to_string( datetime_dt.astimezone(pytz.utc)) self.date_to = datetime_str @api.onchange( 'vacation_date_from', 'vacation_time_from', 'vacation_date_to', 'vacation_time_to', 'number_of_days_temp', 'type', 'holiday_type', 'holiday_status_id') def leave_number_of_days_change(self): if self.type == 'remove': days = self._compute_number_of_days() self.number_of_days_temp = days # Neutralize the native on_change on dates def onchange_date_from(self, cr, uid, ids, date_to, date_from): return {} def onchange_date_to(self, cr, uid, ids, date_to, date_from): return {} # in v8, no more need to inherit check_holidays() as I did in v8 # because it doesn't use number_of_days_temp # I want to set number_of_days_temp as readonly in the view of leaves # and, even in v8, we can't write on a readonly field # So I inherit write and create @api.model def create(self, vals): obj = super(HrHolidays, self).create(vals) if obj.type == 'remove': days = obj._compute_number_of_days() obj.number_of_days_temp = days return obj @api.multi def write(self, vals): res = super(HrHolidays, self).write(vals) for obj in self: if obj.type == 'remove': days = obj._compute_number_of_days() if days != obj.number_of_days_temp: obj.number_of_days_temp = days return res class ResCompany(models.Model): _inherit = 'res.company' mass_allocation_default_holiday_status_id = fields.Many2one( 'hr.holidays.status', string='Default Leave Type for Mass Allocation')