1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669 |
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- # Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
- import logging
- import pytz
- from collections import namedtuple, defaultdict
- from datetime import datetime, timedelta, time
- from pytz import timezone, UTC
- from odoo.tools import date_utils
- from odoo import api, Command, fields, models, tools
- from odoo.addons.base.models.res_partner import _tz_get
- from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
- from odoo.exceptions import AccessError, UserError, ValidationError
- from odoo.tools import float_compare, format_date
- from odoo.tools.float_utils import float_round
- from odoo.tools.misc import format_date
- from odoo.tools.translate import _
- from odoo.osv import expression
- _logger = logging.getLogger(__name__)
- # Used to agglomerate the attendances in order to find the hour_from and hour_to
- # See _compute_date_from_to
- DummyAttendance = namedtuple('DummyAttendance', 'hour_from, hour_to, dayofweek, day_period, week_type')
- def get_employee_from_context(values, context, user_employee_id):
- employee_ids_list = [value[2] for value in values.get('employee_ids', []) if len(value) == 3 and value[0] == Command.SET]
- employee_ids = employee_ids_list[-1] if employee_ids_list else []
- employee_id_value = employee_ids[0] if employee_ids else False
- return employee_id_value or context.get('default_employee_id', context.get('employee_id', user_employee_id))
- class HolidaysRequest(models.Model):
- """ Leave Requests Access specifications
- - a regular employee / user
- - can see all leaves;
- - cannot see name field of leaves belonging to other user as it may contain
- private information that we don't want to share to other people than
- HR people;
- - can modify only its own not validated leaves (except writing on state to
- bypass approval);
- - can discuss on its leave requests;
- - can reset only its own leaves;
- - cannot validate any leaves;
- - an Officer
- - can see all leaves;
- - can validate "HR" single validation leaves from people if
- - he is the employee manager;
- - he is the department manager;
- - he is member of the same department;
- - target employee has no manager and no department manager;
- - can validate "Manager" single validation leaves from people if
- - he is the employee manager;
- - he is the department manager;
- - target employee has no manager and no department manager;
- - can first validate "Both" double validation leaves from people like "HR"
- single validation, moving the leaves to validate1 state;
- - cannot validate its own leaves;
- - can reset only its own leaves;
- - can refuse all leaves;
- - a Manager
- - can do everything he wants
- On top of that multicompany rules apply based on company defined on the
- leave request leave type.
- """
- _name = "hr.leave"
- _description = "Time Off"
- _order = "date_from desc"
- _inherit = ['mail.thread', 'mail.activity.mixin']
- _mail_post_access = 'read'
- @api.model
- def default_get(self, fields_list):
- defaults = super(HolidaysRequest, self).default_get(fields_list)
- defaults = self._default_get_request_parameters(defaults)
- if self.env.context.get('holiday_status_name_get', True) and 'holiday_status_id' in fields_list and not defaults.get('holiday_status_id'):
- lt = self.env['hr.leave.type'].search(['|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)], limit=1, order='sequence')
- if lt:
- defaults['holiday_status_id'] = lt.id
- defaults['request_unit_custom'] = False
- if 'state' in fields_list and not defaults.get('state'):
- lt = self.env['hr.leave.type'].browse(defaults.get('holiday_status_id'))
- defaults['state'] = 'confirm'
- if 'date_from' and 'date_to' in fields_list:
- now = fields.Datetime.now()
- if 'date_from' not in defaults:
- defaults['date_from'] = now.replace(hour=0, minute=0, second=0)
- if 'date_to' not in defaults:
- defaults['date_to'] = now.replace(hour=23, minute=59, second=59)
- if (not self._context.get('leave_compute_date_from_to') and defaults['date_from'].time() == time(0, 0, 0) and
- defaults['date_to'].time() == time(23, 59, 59)):
- employee = self.env['hr.employee'].browse(defaults['employee_id']) if defaults.get('employee_id') else self.env.user.employee_id
- date_from = defaults['date_from'].date()
- date_to = defaults['date_to'].date()
- attendance_from, attendance_to = self._get_attendances(employee, date_from, date_to)
- defaults['date_from'] = self._get_start_or_end_from_attendance(attendance_from.hour_from, date_from, employee)
- defaults['date_to'] = self._get_start_or_end_from_attendance(attendance_to.hour_to, date_to, employee)
- defaults = self._default_get_request_parameters(defaults)
- return defaults
- def _default_get_request_parameters(self, values):
- new_values = dict(values)
- if values.get('date_from') and values.get('date_to'):
- date_from = self._adjust_date_based_on_tz(values['date_from'].date(), values['date_from'].time())
- date_to = self._adjust_date_based_on_tz(values['date_to'].date(), values['date_to'].time())
- new_values.update([('request_date_from', date_from), ('request_date_to', date_to)])
- employee = self.env['hr.employee'].browse(values['employee_id']) if values.get('employee_id') else self.env.user.employee_id
- default_start_time = self._get_start_or_end_from_attendance(7, datetime.now().date(), employee).time()
- default_end_time = self._get_start_or_end_from_attendance(19, datetime.now().date(), employee).time()
- if values['date_from'].time() == default_start_time and values['date_to'].time() == default_end_time:
- attendance_from, attendance_to = self._get_attendances(employee, date_from, date_to)
- new_values['date_from'] = self._get_start_or_end_from_attendance(attendance_from.hour_from, date_from, employee)
- new_values['date_to'] = self._get_start_or_end_from_attendance(attendance_to.hour_to, date_to, employee)
- return new_values
- active = fields.Boolean(default=True, readonly=True)
- # description
- name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False)
- private_name = fields.Char('Time Off Description', groups='hr_holidays.group_hr_holidays_user')
- state = fields.Selection([
- ('draft', 'To Submit'),
- ('confirm', 'To Approve'),
- ('refuse', 'Refused'),
- ('validate1', 'Second Approval'),
- ('validate', 'Approved')
- ], string='Status', compute='_compute_state', store=True, tracking=True, copy=False, readonly=False,
- help="The status is set to 'To Submit', when a time off request is created." +
- "\nThe status is 'To Approve', when time off request is confirmed by user." +
- "\nThe status is 'Refused', when time off request is refused by manager." +
- "\nThe status is 'Approved', when time off request is approved by manager.")
- report_note = fields.Text('HR Comments', copy=False, groups="hr_holidays.group_hr_holidays_manager")
- user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, readonly=True, index=True)
- manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_id', store=True, readonly=False)
- # leave type configuration
- holiday_status_id = fields.Many2one(
- "hr.leave.type", compute='_compute_from_employee_id', store=True, string="Time Off Type", required=True, readonly=False,
- states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]},
- domain="[('company_id', '=?', employee_company_id), '|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)]", tracking=True)
- holiday_allocation_id = fields.Many2one(
- 'hr.leave.allocation', compute='_compute_from_holiday_status_id', string="Allocation", store=True, readonly=False)
- color = fields.Integer("Color", related='holiday_status_id.color')
- validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.leave_validation_type', readonly=False)
- # HR data
- employee_id = fields.Many2one(
- 'hr.employee', compute='_compute_from_employee_ids', store=True, string='Employee', index=True, readonly=False, ondelete="restrict",
- states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]},
- tracking=True, compute_sudo=False)
- employee_company_id = fields.Many2one(related='employee_id.company_id', readonly=True, store=True)
- active_employee = fields.Boolean(related='employee_id.active', string='Employee Active', readonly=True)
- tz_mismatch = fields.Boolean(compute='_compute_tz_mismatch')
- tz = fields.Selection(_tz_get, compute='_compute_tz')
- department_id = fields.Many2one(
- 'hr.department', compute='_compute_department_id', store=True, string='Department', readonly=False,
- states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
- notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
- # duration
- date_from = fields.Datetime(
- 'Start Date', compute='_compute_date_from_to', store=True, readonly=False, index=True, copy=False, required=True, tracking=True,
- states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
- date_to = fields.Datetime(
- 'End Date', compute='_compute_date_from_to', store=True, readonly=False, copy=False, required=True, tracking=True,
- states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
- number_of_days = fields.Float(
- 'Duration (Days)', compute='_compute_number_of_days', store=True, readonly=False, copy=False, tracking=True,
- help='Number of days of the time off request. Used in the calculation. To manually correct the duration, use this field.')
- number_of_days_display = fields.Float(
- 'Duration in days', compute='_compute_number_of_days_display', readonly=True,
- help='Number of days of the time off request according to your working schedule. Used for interface.')
- number_of_hours_display = fields.Float(
- 'Duration in hours', compute='_compute_number_of_hours_display', readonly=True,
- help='Number of hours of the time off request according to your working schedule. Used for interface.')
- number_of_hours_text = fields.Char(compute='_compute_number_of_hours_text')
- duration_display = fields.Char('Requested (Days/Hours)', compute='_compute_duration_display', store=True,
- help="Field allowing to see the leave request duration in days or hours depending on the leave_type_request_unit") # details
- # details
- meeting_id = fields.Many2one('calendar.event', string='Meeting', copy=False)
- parent_id = fields.Many2one('hr.leave', string='Parent', copy=False)
- linked_request_ids = fields.One2many('hr.leave', 'parent_id', string='Linked Requests')
- holiday_type = fields.Selection([
- ('employee', 'By Employee'),
- ('company', 'By Company'),
- ('department', 'By Department'),
- ('category', 'By Employee Tag')],
- string='Allocation Mode', readonly=True, required=True, default='employee',
- states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
- help='By Employee: Allocation/Request for individual Employee, By Employee Tag: Allocation/Request for group of employees in category')
- employee_ids = fields.Many2many(
- 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employees', readonly=False, groups="hr_holidays.group_hr_holidays_user",
- states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
- multi_employee = fields.Boolean(
- compute='_compute_from_employee_ids', store=True, compute_sudo=False,
- help='Holds whether this allocation concerns more than 1 employee')
- category_id = fields.Many2one(
- 'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag',
- states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Category of Employee')
- mode_company_id = fields.Many2one(
- 'res.company', compute='_compute_from_holiday_type', store=True, string='Company Mode',
- states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
- first_approver_id = fields.Many2one(
- 'hr.employee', string='First Approval', readonly=True, copy=False,
- help='This area is automatically filled by the user who validate the time off')
- second_approver_id = fields.Many2one(
- 'hr.employee', string='Second Approval', readonly=True, copy=False,
- help='This area is automatically filled by the user who validate the time off with second level (If time off type need second validation)')
- can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')
- can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
- can_cancel = fields.Boolean('Can Cancel', compute='_compute_can_cancel')
- attachment_ids = fields.One2many('ir.attachment', 'res_id', string="Attachments")
- # To display in form view
- supported_attachment_ids = fields.Many2many(
- 'ir.attachment', string="Attach File", compute='_compute_supported_attachment_ids',
- inverse='_inverse_supported_attachment_ids')
- supported_attachment_ids_count = fields.Integer(compute='_compute_supported_attachment_ids')
- # UX fields
- all_employee_ids = fields.Many2many('hr.employee', compute='_compute_all_employees', compute_sudo=True, context={'active_test': False})
- leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True)
- leave_type_support_document = fields.Boolean(related="holiday_status_id.support_document")
- # Interface fields used when not using hour-based computation
- request_date_from = fields.Date('Request Start Date')
- request_date_to = fields.Date('Request End Date')
- # Interface fields used when using hour-based computation
- request_hour_from = fields.Selection([
- ('0', '12:00 AM'), ('0.5', '12:30 AM'),
- ('1', '1:00 AM'), ('1.5', '1:30 AM'),
- ('2', '2:00 AM'), ('2.5', '2:30 AM'),
- ('3', '3:00 AM'), ('3.5', '3:30 AM'),
- ('4', '4:00 AM'), ('4.5', '4:30 AM'),
- ('5', '5:00 AM'), ('5.5', '5:30 AM'),
- ('6', '6:00 AM'), ('6.5', '6:30 AM'),
- ('7', '7:00 AM'), ('7.5', '7:30 AM'),
- ('8', '8:00 AM'), ('8.5', '8:30 AM'),
- ('9', '9:00 AM'), ('9.5', '9:30 AM'),
- ('10', '10:00 AM'), ('10.5', '10:30 AM'),
- ('11', '11:00 AM'), ('11.5', '11:30 AM'),
- ('12', '12:00 PM'), ('12.5', '12:30 PM'),
- ('13', '1:00 PM'), ('13.5', '1:30 PM'),
- ('14', '2:00 PM'), ('14.5', '2:30 PM'),
- ('15', '3:00 PM'), ('15.5', '3:30 PM'),
- ('16', '4:00 PM'), ('16.5', '4:30 PM'),
- ('17', '5:00 PM'), ('17.5', '5:30 PM'),
- ('18', '6:00 PM'), ('18.5', '6:30 PM'),
- ('19', '7:00 PM'), ('19.5', '7:30 PM'),
- ('20', '8:00 PM'), ('20.5', '8:30 PM'),
- ('21', '9:00 PM'), ('21.5', '9:30 PM'),
- ('22', '10:00 PM'), ('22.5', '10:30 PM'),
- ('23', '11:00 PM'), ('23.5', '11:30 PM')], string='Hour from')
- request_hour_to = fields.Selection([
- ('0', '12:00 AM'), ('0.5', '12:30 AM'),
- ('1', '1:00 AM'), ('1.5', '1:30 AM'),
- ('2', '2:00 AM'), ('2.5', '2:30 AM'),
- ('3', '3:00 AM'), ('3.5', '3:30 AM'),
- ('4', '4:00 AM'), ('4.5', '4:30 AM'),
- ('5', '5:00 AM'), ('5.5', '5:30 AM'),
- ('6', '6:00 AM'), ('6.5', '6:30 AM'),
- ('7', '7:00 AM'), ('7.5', '7:30 AM'),
- ('8', '8:00 AM'), ('8.5', '8:30 AM'),
- ('9', '9:00 AM'), ('9.5', '9:30 AM'),
- ('10', '10:00 AM'), ('10.5', '10:30 AM'),
- ('11', '11:00 AM'), ('11.5', '11:30 AM'),
- ('12', '12:00 PM'), ('12.5', '12:30 PM'),
- ('13', '1:00 PM'), ('13.5', '1:30 PM'),
- ('14', '2:00 PM'), ('14.5', '2:30 PM'),
- ('15', '3:00 PM'), ('15.5', '3:30 PM'),
- ('16', '4:00 PM'), ('16.5', '4:30 PM'),
- ('17', '5:00 PM'), ('17.5', '5:30 PM'),
- ('18', '6:00 PM'), ('18.5', '6:30 PM'),
- ('19', '7:00 PM'), ('19.5', '7:30 PM'),
- ('20', '8:00 PM'), ('20.5', '8:30 PM'),
- ('21', '9:00 PM'), ('21.5', '9:30 PM'),
- ('22', '10:00 PM'), ('22.5', '10:30 PM'),
- ('23', '11:00 PM'), ('23.5', '11:30 PM')], string='Hour to')
- # used only when the leave is taken in half days
- request_date_from_period = fields.Selection([
- ('am', 'Morning'), ('pm', 'Afternoon')],
- string="Date Period Start", default='am')
- # request type
- request_unit_half = fields.Boolean('Half Day', compute='_compute_request_unit_half', store=True, readonly=False)
- request_unit_hours = fields.Boolean('Custom Hours', compute='_compute_request_unit_hours', store=True, readonly=False)
- # view
- is_hatched = fields.Boolean('Hatched', compute='_compute_is_hatched')
- is_striked = fields.Boolean('Striked', compute='_compute_is_hatched')
- has_stress_day = fields.Boolean(compute='_compute_has_stress_day')
- _sql_constraints = [
- ('type_value',
- "CHECK((holiday_type='employee' AND (employee_id IS NOT NULL OR multi_employee IS TRUE)) or "
- "(holiday_type='company' AND mode_company_id IS NOT NULL) or "
- "(holiday_type='category' AND category_id IS NOT NULL) or "
- "(holiday_type='department' AND department_id IS NOT NULL) )",
- "The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."),
- ('date_check2', "CHECK ((date_from <= date_to))", "The start date must be anterior to the end date."),
- ('duration_check', "CHECK ( number_of_days >= 0 )", "If you want to change the number of days you should use the 'period' mode"),
- ]
- def _auto_init(self):
- res = super(HolidaysRequest, self)._auto_init()
- tools.create_index(self._cr, 'hr_leave_date_to_date_from_index',
- self._table, ['date_to', 'date_from'])
- return res
- @api.depends('employee_id', 'employee_ids')
- def _compute_all_employees(self):
- for leave in self:
- leave.all_employee_ids = leave.employee_id | leave.employee_ids
- @api.constrains('holiday_status_id', 'number_of_days')
- def _check_allocation_duration(self):
- # Deprecated as part of https://github.com/odoo/odoo/pull/96545
- # TODO: remove in master
- return
- @api.depends_context('uid')
- def _compute_description(self):
- self.check_access_rights('read')
- self.check_access_rule('read')
- is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user')
- for leave in self:
- if is_officer or leave.user_id == self.env.user or leave.employee_id.leave_manager_id == self.env.user:
- leave.name = leave.sudo().private_name
- else:
- leave.name = '*****'
- def _inverse_description(self):
- is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user')
- for leave in self:
- if is_officer or leave.user_id == self.env.user or leave.employee_id.leave_manager_id == self.env.user:
- leave.sudo().private_name = leave.name
- def _search_description(self, operator, value):
- is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user')
- domain = [('private_name', operator, value)]
- if not is_officer:
- domain = expression.AND([domain, [('user_id', '=', self.env.user.id)]])
- leaves = self.search(domain)
- return [('id', 'in', leaves.ids)]
- @api.depends('holiday_status_id')
- def _compute_state(self):
- for leave in self:
- leave.state = 'confirm' if leave.validation_type != 'no_validation' else 'draft'
- @api.depends('holiday_status_id.requires_allocation', 'validation_type', 'employee_id', 'date_from', 'date_to')
- def _compute_from_holiday_status_id(self):
- # Deprecated as part of https://github.com/odoo/odoo/pull/96545
- # TODO: remove in master
- self.holiday_allocation_id = False
- @api.depends('request_date_from_period', 'request_hour_from', 'request_hour_to', 'request_date_from', 'request_date_to',
- 'request_unit_half', 'request_unit_hours', 'employee_id')
- def _compute_date_from_to(self):
- for holiday in self:
- if holiday.request_date_from and holiday.request_date_to and holiday.request_date_from > holiday.request_date_to:
- holiday.request_date_to = holiday.request_date_from
- if not holiday.request_date_from:
- holiday.date_from = False
- elif not holiday.request_unit_half and not holiday.request_unit_hours and not holiday.request_date_to:
- holiday.date_to = False
- else:
- if holiday.request_unit_half or holiday.request_unit_hours:
- holiday.request_date_to = holiday.request_date_from
- attendance_from, attendance_to = holiday._get_attendances(holiday.employee_id, holiday.request_date_from, holiday.request_date_to)
- compensated_request_date_from = holiday.request_date_from
- compensated_request_date_to = holiday.request_date_to
- if holiday.request_unit_half:
- if holiday.request_date_from_period == 'am':
- hour_from = attendance_from.hour_from
- hour_to = attendance_from.hour_to
- else:
- hour_from = attendance_to.hour_from
- hour_to = attendance_to.hour_to
- elif holiday.request_unit_hours:
- hour_from = holiday.request_hour_from
- hour_to = holiday.request_hour_to
- else:
- hour_from = attendance_from.hour_from
- hour_to = attendance_to.hour_to
- holiday.date_from = self._get_start_or_end_from_attendance(hour_from, compensated_request_date_from, holiday.employee_id or holiday)
- holiday.date_to = self._get_start_or_end_from_attendance(hour_to, compensated_request_date_to, holiday.employee_id or holiday)
- @api.depends('holiday_status_id', 'request_unit_hours')
- def _compute_request_unit_half(self):
- for holiday in self:
- if holiday.holiday_status_id or holiday.request_unit_hours:
- holiday.request_unit_half = False
- @api.depends('holiday_status_id', 'request_unit_half')
- def _compute_request_unit_hours(self):
- for holiday in self:
- if holiday.holiday_status_id or holiday.request_unit_half:
- holiday.request_unit_hours = False
- @api.depends('employee_ids')
- def _compute_from_employee_ids(self):
- for holiday in self:
- if len(holiday.employee_ids) == 1:
- holiday.employee_id = holiday.employee_ids[0]._origin
- else:
- holiday.employee_id = False
- holiday.multi_employee = (len(holiday.employee_ids) > 1)
- @api.depends('holiday_type')
- def _compute_from_holiday_type(self):
- allocation_from_domain = self.env['hr.leave.allocation']
- if (self._context.get('active_model') == 'hr.leave.allocation' and
- self._context.get('active_id')):
- allocation_from_domain = allocation_from_domain.browse(self._context['active_id'])
- for holiday in self:
- if holiday.holiday_type == 'employee':
- if not holiday.employee_ids:
- if allocation_from_domain:
- holiday.employee_ids = allocation_from_domain.employee_id
- holiday.holiday_status_id = allocation_from_domain.holiday_status_id
- else:
- # This handles the case where a request is made with only the employee_id
- # but does not need to be recomputed on employee_id changes
- holiday.employee_ids = holiday.employee_id or self.env.user.employee_id
- holiday.mode_company_id = False
- holiday.category_id = False
- elif holiday.holiday_type == 'company':
- holiday.employee_ids = False
- if not holiday.mode_company_id:
- holiday.mode_company_id = self.env.company.id
- holiday.category_id = False
- elif holiday.holiday_type == 'department':
- holiday.employee_ids = False
- holiday.mode_company_id = False
- holiday.category_id = False
- elif holiday.holiday_type == 'category':
- holiday.employee_ids = False
- holiday.mode_company_id = False
- else:
- holiday.employee_ids = self.env.context.get('default_employee_id') or holiday.employee_id or self.env.user.employee_id
- @api.depends('employee_id', 'employee_ids')
- def _compute_from_employee_id(self):
- for holiday in self:
- holiday.manager_id = holiday.employee_id.parent_id.id
- if holiday.holiday_status_id.requires_allocation == 'no':
- continue
- if not holiday.employee_id or holiday.employee_ids:
- holiday.holiday_status_id = False
- elif holiday.employee_id.user_id != self.env.user and holiday._origin.employee_id != holiday.employee_id:
- if holiday.employee_id and not holiday.holiday_status_id.with_context(employee_id=holiday.employee_id.id).has_valid_allocation:
- holiday.holiday_status_id = False
- @api.depends('employee_id', 'holiday_type')
- def _compute_department_id(self):
- for holiday in self:
- if holiday.employee_id:
- holiday.department_id = holiday.employee_id.department_id
- elif holiday.holiday_type == 'department':
- if not holiday.department_id:
- holiday.department_id = self.env.user.employee_id.department_id
- else:
- holiday.department_id = False
- @api.depends('date_from', 'date_to', 'holiday_status_id')
- def _compute_has_stress_day(self):
- date_from, date_to = min(self.mapped('date_from')), max(self.mapped('date_to'))
- if date_from and date_to:
- stress_days = self.employee_id._get_stress_days(
- date_from.date(),
- date_to.date())
- for leave in self:
- domain = [
- ('start_date', '<=', leave.date_to.date()),
- ('end_date', '>=', leave.date_from.date()),
- '|',
- ('resource_calendar_id', '=', False),
- ('resource_calendar_id', '=', (leave.employee_id.resource_calendar_id or self.env.company.resource_calendar_id).id)
- ]
- if leave.holiday_status_id.company_id:
- domain += [('company_id', '=', leave.holiday_status_id.company_id.id)]
- leave.has_stress_day = leave.date_from and leave.date_to and stress_days.filtered_domain(domain)
- else:
- self.has_stress_day = False
- @api.depends('date_from', 'date_to', 'employee_id')
- def _compute_number_of_days(self):
- for holiday in self:
- if holiday.date_from and holiday.date_to:
- holiday.number_of_days = holiday._get_number_of_days(holiday.date_from, holiday.date_to, holiday.employee_id.id)['days']
- else:
- holiday.number_of_days = 0
- @api.depends('tz')
- @api.depends_context('uid')
- def _compute_tz_mismatch(self):
- for leave in self:
- leave.tz_mismatch = leave.tz != self.env.user.tz
- @api.depends('employee_id', 'holiday_type', 'department_id.company_id.resource_calendar_id.tz', 'mode_company_id.resource_calendar_id.tz')
- def _compute_tz(self):
- for leave in self:
- tz = False
- if leave.holiday_type == 'employee':
- tz = leave.employee_id.tz
- elif leave.holiday_type == 'department':
- tz = leave.department_id.company_id.resource_calendar_id.tz
- elif leave.holiday_type == 'company':
- tz = leave.mode_company_id.resource_calendar_id.tz
- leave.tz = tz or self.env.company.resource_calendar_id.tz or self.env.user.tz or 'UTC'
- @api.depends('number_of_days')
- def _compute_number_of_days_display(self):
- for holiday in self:
- holiday.number_of_days_display = holiday.number_of_days
- def _get_calendar(self):
- self.ensure_one()
- return self.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
- @api.depends('number_of_days')
- def _compute_number_of_hours_display(self):
- for holiday in self:
- calendar = holiday._get_calendar()
- if holiday.date_from and holiday.date_to:
- # Take attendances into account, in case the leave validated
- # Otherwise, this will result into number_of_hours = 0
- # and number_of_hours_display = 0 or (#day * calendar.hours_per_day),
- # which could be wrong if the employee doesn't work the same number
- # hours each day
- if holiday.state == 'validate':
- start_dt = holiday.date_from
- end_dt = holiday.date_to
- if not start_dt.tzinfo:
- start_dt = start_dt.replace(tzinfo=UTC)
- if not end_dt.tzinfo:
- end_dt = end_dt.replace(tzinfo=UTC)
- resource = holiday.employee_id.resource_id
- intervals = calendar._attendance_intervals_batch(start_dt, end_dt, resource)[resource.id] \
- - calendar._leave_intervals_batch(start_dt, end_dt, None)[False] # Substract Global Leaves
- number_of_hours = sum((stop - start).total_seconds() / 3600 for start, stop, dummy in intervals)
- else:
- number_of_hours = holiday._get_number_of_days(holiday.date_from, holiday.date_to, holiday.employee_id.id)['hours']
- holiday.number_of_hours_display = number_of_hours or (holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY))
- else:
- holiday.number_of_hours_display = 0
- @api.depends('number_of_hours_display', 'number_of_days_display')
- def _compute_duration_display(self):
- for leave in self:
- leave.duration_display = '%g %s' % (
- (float_round(leave.number_of_hours_display, precision_digits=2)
- if leave.leave_type_request_unit == 'hour'
- else float_round(leave.number_of_days_display, precision_digits=2)),
- _('hours') if leave.leave_type_request_unit == 'hour' else _('days'))
- @api.depends('number_of_hours_display')
- def _compute_number_of_hours_text(self):
- # YTI Note: All this because a readonly field takes all the width on edit mode...
- for leave in self:
- leave.number_of_hours_text = '%s%g %s%s' % (
- '' if leave.request_unit_half or leave.request_unit_hours else '(',
- float_round(leave.number_of_hours_display, precision_digits=2),
- _('Hours'),
- '' if leave.request_unit_half or leave.request_unit_hours else ')')
- @api.depends('state', 'employee_id', 'department_id')
- def _compute_can_reset(self):
- for holiday in self:
- try:
- holiday._check_approval_update('draft')
- except (AccessError, UserError):
- holiday.can_reset = False
- else:
- holiday.can_reset = True
- @api.depends('state', 'employee_id', 'department_id')
- def _compute_can_approve(self):
- for holiday in self:
- try:
- if holiday.state == 'confirm' and holiday.validation_type == 'both':
- holiday._check_approval_update('validate1')
- else:
- holiday._check_approval_update('validate')
- except (AccessError, UserError):
- holiday.can_approve = False
- else:
- holiday.can_approve = True
- @api.depends_context('uid')
- @api.depends('state', 'employee_id')
- def _compute_can_cancel(self):
- now = fields.Datetime.now()
- for leave in self:
- leave.can_cancel = leave.id and leave.employee_id.user_id == self.env.user and leave.state == 'validate' and leave.date_from and leave.date_from > now
- @api.depends('state')
- def _compute_is_hatched(self):
- for holiday in self:
- holiday.is_striked = holiday.state == 'refuse'
- holiday.is_hatched = holiday.state not in ['refuse', 'validate']
- @api.depends('leave_type_support_document', 'attachment_ids')
- def _compute_supported_attachment_ids(self):
- for holiday in self:
- holiday.supported_attachment_ids = holiday.attachment_ids
- holiday.supported_attachment_ids_count = len(holiday.attachment_ids.ids)
- def _inverse_supported_attachment_ids(self):
- for holiday in self:
- holiday.attachment_ids = holiday.supported_attachment_ids
- @api.constrains('date_from', 'date_to', 'employee_id')
- def _check_date(self):
- if self.env.context.get('leave_skip_date_check', False):
- return
- all_employees = self.employee_id | self.employee_ids
- all_leaves = self.search([
- ('date_from', '<', max(self.mapped('date_to'))),
- ('date_to', '>', min(self.mapped('date_from'))),
- ('employee_id', 'in', all_employees.ids),
- ('id', 'not in', self.ids),
- ('state', 'not in', ['cancel', 'refuse']),
- ])
- for holiday in self:
- domain = [
- ('date_from', '<', holiday.date_to),
- ('date_to', '>', holiday.date_from),
- ('id', '!=', holiday.id),
- ('state', 'not in', ['cancel', 'refuse']),
- ]
- employee_ids = (holiday.employee_id | holiday.employee_ids).ids
- search_domain = domain + [('employee_id', 'in', employee_ids)]
- conflicting_holidays = all_leaves.filtered_domain(search_domain)
- if conflicting_holidays:
- conflicting_holidays_list = []
- # Do not display the name of the employee if the conflicting holidays have an employee_id.user_id equivalent to the user id
- holidays_only_have_uid = bool(holiday.employee_id)
- holiday_states = dict(conflicting_holidays.fields_get(allfields=['state'])['state']['selection'])
- for conflicting_holiday in conflicting_holidays:
- conflicting_holiday_data = {}
- conflicting_holiday_data['employee_name'] = conflicting_holiday.employee_id.name
- conflicting_holiday_data['date_from'] = format_date(self.env, min(conflicting_holiday.mapped('date_from')))
- conflicting_holiday_data['date_to'] = format_date(self.env, min(conflicting_holiday.mapped('date_to')))
- conflicting_holiday_data['state'] = holiday_states[conflicting_holiday.state]
- if conflicting_holiday.employee_id.user_id.id != self.env.uid:
- holidays_only_have_uid = False
- if conflicting_holiday_data not in conflicting_holidays_list:
- conflicting_holidays_list.append(conflicting_holiday_data)
- if not conflicting_holidays_list:
- return
- conflicting_holidays_strings = []
- if holidays_only_have_uid:
- for conflicting_holiday_data in conflicting_holidays_list:
- conflicting_holidays_string = _('From %(date_from)s To %(date_to)s - %(state)s',
- date_from=conflicting_holiday_data['date_from'],
- date_to=conflicting_holiday_data['date_to'],
- state=conflicting_holiday_data['state'])
- conflicting_holidays_strings.append(conflicting_holidays_string)
- raise ValidationError(_('You can not set two time off that overlap on the same day.\nExisting time off:\n%s') %
- ('\n'.join(conflicting_holidays_strings)))
- for conflicting_holiday_data in conflicting_holidays_list:
- conflicting_holidays_string = _('%(employee_name)s - From %(date_from)s To %(date_to)s - %(state)s',
- employee_name=conflicting_holiday_data['employee_name'],
- date_from=conflicting_holiday_data['date_from'],
- date_to=conflicting_holiday_data['date_to'],
- state=conflicting_holiday_data['state'])
- conflicting_holidays_strings.append(conflicting_holidays_string)
- conflicting_employees = set(employee_ids) - set(conflicting_holidays.employee_id.ids)
- # Only one employee has a conflicting holiday
- if len(conflicting_employees) == len(employee_ids) - 1:
- raise ValidationError(_('You can not set two time off that overlap on the same day for the same employee.\nExisting time off:\n%s') %
- ('\n'.join(conflicting_holidays_strings)))
- raise ValidationError(_('You can not set two time off that overlap on the same day for the same employees.\nExisting time off:\n%s') %
- ('\n'.join(conflicting_holidays_strings)))
- @api.constrains('state', 'number_of_days', 'holiday_status_id')
- def _check_holidays(self):
- for holiday in self:
- mapped_days = self.holiday_status_id.get_employees_days((holiday.employee_id | holiday.sudo().employee_ids).ids, holiday.date_from.date())
- if holiday.holiday_type != 'employee'\
- or not holiday.employee_id and not holiday.sudo().employee_ids\
- or holiday.holiday_status_id.requires_allocation == 'no':
- continue
- if holiday.employee_id:
- leave_days = mapped_days[holiday.employee_id.id][holiday.holiday_status_id.id]
- if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1\
- or float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1:
- raise ValidationError(_('The number of remaining time off is not sufficient for this time off type.\n'
- 'Please also check the time off waiting for validation.'))
- else:
- unallocated_employees = []
- for employee in holiday.sudo().employee_ids:
- leave_days = mapped_days[employee.id][holiday.holiday_status_id.id]
- if float_compare(leave_days['remaining_leaves'], self.number_of_days, precision_digits=2) == -1\
- or float_compare(leave_days['virtual_remaining_leaves'], self.number_of_days, precision_digits=2) == -1:
- unallocated_employees.append(employee.name)
- if unallocated_employees:
- raise ValidationError(_('The number of remaining time off is not sufficient for this time off type.\n'
- 'Please also check the time off waiting for validation.')
- + _('\nThe employees that lack allocation days are:\n%s',
- (', '.join(unallocated_employees))))
- @api.constrains('date_from', 'date_to', 'employee_id')
- def _check_date_state(self):
- if self.env.context.get('leave_skip_state_check'):
- return
- for holiday in self:
- if holiday.state in ['cancel', 'refuse', 'validate1', 'validate']:
- raise ValidationError(_("This modification is not allowed in the current state."))
- def _get_number_of_days_batch(self, date_from, date_to, employee_ids):
- """ Returns a float equals to the timedelta between two dates given as string."""
- employee = self.env['hr.employee'].browse(employee_ids)
- # We force the company in the domain as we are more than likely in a compute_sudo
- domain = [('time_type', '=', 'leave'),
- ('company_id', 'in', self.env.company.ids + self.env.context.get('allowed_company_ids', []))]
- result = employee._get_work_days_data_batch(date_from, date_to, domain=domain)
- for employee_id in result:
- if self.request_unit_half and result[employee_id]['hours'] > 0:
- result[employee_id]['days'] = 0.5
- return result
- def _get_number_of_days(self, date_from, date_to, employee_id):
- """ Returns a float equals to the timedelta between two dates given as string."""
- if employee_id:
- return self._get_number_of_days_batch(date_from, date_to, employee_id)[employee_id]
- today_hours = self.env.company.resource_calendar_id.get_work_hours_count(
- datetime.combine(date_from.date(), time.min),
- datetime.combine(date_from.date(), time.max),
- False)
- hours = self.env.company.resource_calendar_id.get_work_hours_count(date_from, date_to)
- days = hours / (today_hours or HOURS_PER_DAY) if not self.request_unit_half else 0.5
- return {'days': days, 'hours': hours}
- def _adjust_date_based_on_tz(self, leave_date, hour):
- """ request_date_{from,to} are local to the user's tz but hour_{from,to} are in UTC.
- In some cases they are combined (assuming they are in the same tz) as a datetime. When
- that happens it's possible we need to adjust one of the dates. This function adjust the
- date, so that it can be passed to datetime().
- E.g. a leave in US/Pacific for one day:
- - request_date_from: 1st of Jan
- - request_date_to: 1st of Jan
- - hour_from: 15:00 (7:00 local)
- - hour_to: 03:00 (19:00 local) <-- this happens on the 2nd of Jan in UTC
- """
- user_tz = timezone(self.env.user.tz if self.env.user.tz else 'UTC')
- request_date_to_utc = UTC.localize(datetime.combine(leave_date, hour)).astimezone(user_tz).replace(tzinfo=None)
- return request_date_to_utc.date()
- ####################################################
- # ORM Overrides methods
- ####################################################
- @api.depends('employee_id', 'holiday_status_id')
- def _compute_display_name(self):
- super()._compute_display_name()
- def onchange(self, values, field_name, field_onchange):
- # Try to force the leave_type name_get when creating new records
- # This is called right after pressing create and returns the name_get for
- # most fields in the view.
- if field_onchange.get('employee_id') and 'employee_id' not in self._context and values:
- employee_id = get_employee_from_context(values, self._context, self.env.user.employee_id.id)
- self = self.with_context(employee_id=employee_id)
- return super().onchange(values, field_name, field_onchange)
- def name_get(self):
- res = []
- for leave in self:
- user_tz = timezone(leave.tz)
- date_from_utc = leave.date_from and leave.date_from.astimezone(user_tz).date()
- date_to_utc = leave.date_to and leave.date_to.astimezone(user_tz).date()
- if self.env.context.get('short_name'):
- if leave.leave_type_request_unit == 'hour':
- res.append((leave.id, _("%s : %.2f hours") % (leave.name or leave.holiday_status_id.name, leave.number_of_hours_display)))
- else:
- res.append((leave.id, _("%s : %.2f days") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
- else:
- if leave.holiday_type == 'company':
- target = leave.mode_company_id.name
- elif leave.holiday_type == 'department':
- target = leave.department_id.name
- elif leave.holiday_type == 'category':
- target = leave.category_id.name
- elif leave.employee_id:
- target = leave.employee_id.name
- else:
- target = ', '.join(leave.employee_ids.mapped('name'))
- display_date = format_date(self.env, date_from_utc) or ""
- if leave.leave_type_request_unit == 'hour':
- if self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []):
- res.append((
- leave.id,
- _("%(person)s on %(leave_type)s: %(duration).2f hours on %(date)s",
- person=target,
- leave_type=leave.holiday_status_id.name,
- duration=leave.number_of_hours_display,
- date=display_date,
- )
- ))
- else:
- res.append((
- leave.id,
- _("%(person)s on %(leave_type)s: %(duration).2f hours on %(date)s",
- person=target,
- leave_type=leave.holiday_status_id.name,
- duration=leave.number_of_hours_display,
- date=display_date,
- )
- ))
- else:
- if leave.number_of_days > 1 and date_from_utc and date_to_utc:
- display_date += ' / %s' % format_date(self.env, date_to_utc) or ""
- if not target or self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []):
- res.append((
- leave.id,
- _("%(leave_type)s: %(duration).2f days (%(start)s)",
- leave_type=leave.holiday_status_id.name,
- duration=leave.number_of_days,
- start=display_date,
- )
- ))
- else:
- res.append((
- leave.id,
- _("%(person)s on %(leave_type)s: %(duration).2f days (%(start)s)",
- person=target,
- leave_type=leave.holiday_status_id.name,
- duration=leave.number_of_days,
- start=display_date,
- )
- ))
- return res
- def add_follower(self, employee_id):
- employee = self.env['hr.employee'].browse(employee_id)
- if employee.user_id:
- self.message_subscribe(partner_ids=employee.user_id.partner_id.ids)
- @api.constrains('holiday_allocation_id')
- def _check_allocation_id(self):
- # Deprecated as part of https://github.com/odoo/odoo/pull/96545
- # TODO: remove in master
- return
- @api.constrains('holiday_allocation_id', 'date_to', 'date_from')
- def _check_leave_type_validity(self):
- # Deprecated as part of https://github.com/odoo/odoo/pull/96545
- # TODO: remove in master
- return
- @api.constrains('date_from', 'date_to')
- def _check_stress_day(self):
- is_leave_user = self.user_has_groups('hr_holidays.group_hr_holidays_user')
- if not is_leave_user and any(leave.has_stress_day for leave in self):
- raise ValidationError(_('You are not allowed to request a time off on a Stress Day.'))
- def _check_double_validation_rules(self, employees, state):
- if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
- return
- is_leave_user = self.user_has_groups('hr_holidays.group_hr_holidays_user')
- if state == 'validate1':
- employees = employees.filtered(lambda employee: employee.leave_manager_id != self.env.user)
- if employees and not is_leave_user:
- raise AccessError(_('You cannot first approve a time off for %s, because you are not his time off manager', employees[0].name))
- elif state == 'validate' and not is_leave_user:
- # Is probably handled via ir.rule
- raise AccessError(_('You don\'t have the rights to apply second approval on a time off request'))
- @api.model_create_multi
- def create(self, vals_list):
- leave_date_employees = defaultdict(list)
- employee_ids = []
- for values in vals_list:
- if values.get('employee_id'):
- employee_ids.append(values['employee_id'])
- if values.get('date_from') and values.get('date_to'):
- date_from = fields.Datetime.to_datetime(values['date_from'])
- date_to = fields.Datetime.to_datetime(values['date_to'])
- if values['employee_id'] not in leave_date_employees[(date_from, date_to)]:
- leave_date_employees[(date_from, date_to)].append(values['employee_id'])
- employees = self.env['hr.employee'].browse(employee_ids)
- if self._context.get('leave_compute_date_from_to') and employees:
- employee_leave_date_duration = defaultdict(dict)
- for (date_from, date_to), employee_ids in leave_date_employees.items():
- employee_leave_date_duration[(date_from, date_to)] = self._get_number_of_days_batch(date_from, date_to, employee_ids)
- for values in vals_list:
- employee_id = values.get('employee_id')
- if employee_id and values.get('date_from') and values.get('date_to'):
- date_from = values.get('date_from')
- date_to = values.get('date_to')
- employee = employees.filtered(lambda emp: emp.id == employee_id)
- attendance_from, attendance_to = self._get_attendances(employee, date_from.date(), date_to.date())
- hour_from = float_to_time(attendance_from.hour_from)
- hour_to = float_to_time(attendance_to.hour_to)
- hour_from = hour_from.hour + hour_from.minute / 60
- hour_to = hour_to.hour + hour_to.minute / 60
- values['date_from'] = self._get_start_or_end_from_attendance(hour_from, date_from.date(), employee)
- values['date_to'] = self._get_start_or_end_from_attendance(hour_to, date_to.date(), employee)
- values['request_date_from'], values['request_date_to'] = values['date_from'].date(), values['date_to'].date()
- values['number_of_days'] = employee_leave_date_duration[(date_from, date_to)][values['employee_id']]['days']
- """ Override to avoid automatic logging of creation """
- if not self._context.get('leave_fast_create'):
- leave_types = self.env['hr.leave.type'].browse([values.get('holiday_status_id') for values in vals_list if values.get('holiday_status_id')])
- mapped_validation_type = {leave_type.id: leave_type.leave_validation_type for leave_type in leave_types}
- for values in vals_list:
- employee_id = values.get('employee_id', False)
- leave_type_id = values.get('holiday_status_id')
- # Handle automatic department_id
- if not values.get('department_id'):
- values.update({'department_id': employees.filtered(lambda emp: emp.id == employee_id).department_id.id})
- # Handle no_validation
- if mapped_validation_type[leave_type_id] == 'no_validation':
- values.update({'state': 'confirm'})
- if 'state' not in values:
- # To mimic the behavior of compute_state that was always triggered, as the field was readonly
- values['state'] = 'confirm' if mapped_validation_type[leave_type_id] != 'no_validation' else 'draft'
- # Handle double validation
- if mapped_validation_type[leave_type_id] == 'both':
- self._check_double_validation_rules(employee_id, values.get('state', False))
- holidays = super(HolidaysRequest, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
- for holiday in holidays:
- if not self._context.get('leave_fast_create'):
- # Everything that is done here must be done using sudo because we might
- # have different create and write rights
- # eg : holidays_user can create a leave request with validation_type = 'manager' for someone else
- # but they can only write on it if they are leave_manager_id
- holiday_sudo = holiday.sudo()
- holiday_sudo.add_follower(employee_id)
- if holiday.validation_type == 'manager':
- holiday_sudo.message_subscribe(partner_ids=holiday.employee_id.leave_manager_id.partner_id.ids)
- if holiday.validation_type == 'no_validation':
- # Automatic validation should be done in sudo, because user might not have the rights to do it by himself
- holiday_sudo.action_validate()
- holiday_sudo.message_subscribe(partner_ids=[holiday._get_responsible_for_approval().partner_id.id])
- holiday_sudo.message_post(body=_("The time off has been automatically approved"), subtype_xmlid="mail.mt_comment") # Message from OdooBot (sudo)
- elif not self._context.get('import_file'):
- holiday_sudo.activity_update()
- return holidays
- def write(self, values):
- if 'active' in values and not self.env.context.get('from_cancel_wizard'):
- raise UserError(_("You can't manually archive/unarchive a time off."))
- is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user') or self.env.is_superuser()
- if not is_officer and values.keys() - {'attachment_ids', 'supported_attachment_ids', 'message_main_attachment_id'}:
- if any(hol.date_from.date() < fields.Date.today() and hol.employee_id.leave_manager_id != self.env.user for hol in self):
- raise UserError(_('You must have manager rights to modify/validate a time off that already begun'))
- # Unlink existing resource.calendar.leaves for validated time off
- if 'state' in values and values['state'] != 'validate':
- validated_leaves = self.filtered(lambda l: l.state == 'validate')
- validated_leaves._remove_resource_leave()
- employee_id = values.get('employee_id', False)
- if not self.env.context.get('leave_fast_create'):
- if values.get('state'):
- self._check_approval_update(values['state'])
- if any(holiday.validation_type == 'both' for holiday in self):
- if values.get('employee_id'):
- employees = self.env['hr.employee'].browse(values.get('employee_id'))
- else:
- employees = self.mapped('employee_id')
- self._check_double_validation_rules(employees, values['state'])
- if 'date_from' in values:
- values['request_date_from'] = values['date_from']
- if 'date_to' in values:
- values['request_date_to'] = values['date_to']
- result = super(HolidaysRequest, self).write(values)
- if not self.env.context.get('leave_fast_create'):
- for holiday in self:
- if employee_id:
- holiday.add_follower(employee_id)
- return result
- @api.ondelete(at_uninstall=False)
- def _unlink_if_correct_states(self):
- error_message = _('You cannot delete a time off which is in %s state')
- state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
- now = fields.Datetime.now()
- if not self.user_has_groups('hr_holidays.group_hr_holidays_user'):
- for hol in self:
- if hol.state not in ['draft', 'confirm']:
- raise UserError(error_message % state_description_values.get(self[:1].state))
- if hol.date_from < now:
- raise UserError(_('You cannot delete a time off which is in the past'))
- if hol.sudo().employee_ids and not hol.employee_id:
- raise UserError(_('You cannot delete a time off assigned to several employees'))
- else:
- for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']):
- raise UserError(error_message % (state_description_values.get(holiday.state),))
- def unlink(self):
- return super(HolidaysRequest, self.with_context(leave_skip_date_check=True)).unlink()
- def copy_data(self, default=None):
- if default and 'date_from' in default and 'date_to' in default:
- default['request_date_from'] = default.get('date_from')
- default['request_date_to'] = default.get('date_to')
- return super().copy_data(default)
- raise UserError(_('A time off cannot be duplicated.'))
- def _get_mail_redirect_suggested_company(self):
- return self.holiday_status_id.company_id
- @api.model
- def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
- if not self.user_has_groups('hr_holidays.group_hr_holidays_user') and 'private_name' in groupby:
- raise UserError(_('Such grouping is not allowed.'))
- return super(HolidaysRequest, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
- ####################################################
- # Business methods
- ####################################################
- def _prepare_resource_leave_vals_list(self):
- """Hook method for others to inject data
- """
- return [{
- 'name': _("%s: Time Off", leave.employee_id.name),
- 'date_from': leave.date_from,
- 'holiday_id': leave.id,
- 'date_to': leave.date_to,
- 'resource_id': leave.employee_id.resource_id.id,
- 'calendar_id': leave.employee_id.resource_calendar_id.id,
- 'time_type': leave.holiday_status_id.time_type,
- } for leave in self]
- def _create_resource_leave(self):
- """ This method will create entry in resource calendar time off object at the time of holidays validated
- :returns: created `resource.calendar.leaves`
- """
- vals_list = self._prepare_resource_leave_vals_list()
- return self.env['resource.calendar.leaves'].sudo().create(vals_list)
- def _remove_resource_leave(self):
- """ This method will create entry in resource calendar time off object at the time of holidays cancel/removed """
- return self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)]).unlink()
- def _validate_leave_request(self):
- """ Validate time off requests (holiday_type='employee')
- by creating a calendar event and a resource time off. """
- holidays = self.filtered(lambda request: request.holiday_type == 'employee' and request.employee_id)
- holidays._create_resource_leave()
- meeting_holidays = holidays.filtered(lambda l: l.holiday_status_id.create_calendar_meeting)
- meetings = self.env['calendar.event']
- if meeting_holidays:
- meeting_values_for_user_id = meeting_holidays._prepare_holidays_meeting_values()
- Meeting = self.env['calendar.event']
- for user_id, meeting_values in meeting_values_for_user_id.items():
- meetings += Meeting.with_user(user_id or self.env.uid).with_context(
- allowed_company_ids=[],
- no_mail_to_attendees=True,
- calendar_no_videocall=True,
- active_model=self._name
- ).create(meeting_values)
- Holiday = self.env['hr.leave']
- for meeting in meetings:
- Holiday.browse(meeting.res_id).meeting_id = meeting
- def _prepare_holidays_meeting_values(self):
- result = defaultdict(list)
- company_calendar = self.env.company.resource_calendar_id
- for holiday in self:
- calendar = holiday.employee_id.resource_calendar_id or company_calendar
- user = holiday.user_id
- if holiday.leave_type_request_unit == 'hour':
- meeting_name = _("%s on Time Off : %.2f hour(s)") % (holiday.employee_id.name or holiday.category_id.name, holiday.number_of_hours_display)
- else:
- meeting_name = _("%s on Time Off : %.2f day(s)") % (holiday.employee_id.name or holiday.category_id.name, holiday.number_of_days)
- meeting_values = {
- 'name': meeting_name,
- 'duration': holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY),
- 'description': holiday.notes,
- 'user_id': user.id,
- 'start': holiday.date_from,
- 'stop': holiday.date_to,
- 'allday': False,
- 'privacy': 'confidential',
- 'event_tz': user.tz,
- 'activity_ids': [(5, 0, 0)],
- 'res_id': holiday.id,
- }
- # Add the partner_id (if exist) as an attendee
- if user and user.partner_id:
- meeting_values['partner_ids'] = [
- (4, user.partner_id.id)]
- result[user.id].append(meeting_values)
- return result
- def _prepare_employees_holiday_values(self, employees):
- self.ensure_one()
- work_days_data = employees._get_work_days_data_batch(self.date_from, self.date_to)
- return [{
- 'name': self.name,
- 'holiday_type': 'employee',
- 'holiday_status_id': self.holiday_status_id.id,
- 'date_from': self.date_from,
- 'date_to': self.date_to,
- 'request_date_from': self.request_date_from,
- 'request_date_to': self.request_date_to,
- 'notes': self.notes,
- 'number_of_days': work_days_data[employee.id]['days'],
- 'parent_id': self.id,
- 'employee_id': employee.id,
- 'employee_ids': employee,
- 'state': 'validate',
- } for employee in employees if work_days_data[employee.id]['days']]
- def action_cancel(self):
- self.ensure_one()
- return {
- 'name': _('Cancel Time Off'),
- 'type': 'ir.actions.act_window',
- 'target': 'new',
- 'res_model': 'hr.holidays.cancel.leave',
- 'view_mode': 'form',
- 'views': [[False, 'form']],
- 'context': {
- 'default_leave_id': self.id,
- }
- }
- def action_draft(self):
- if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
- raise UserError(_('Time off request state must be "Refused" or "To Approve" in order to be reset to draft.'))
- self.write({
- 'state': 'draft',
- 'first_approver_id': False,
- 'second_approver_id': False,
- })
- linked_requests = self.mapped('linked_request_ids')
- if linked_requests:
- linked_requests.action_draft()
- linked_requests.unlink()
- self.activity_update()
- return True
- def action_confirm(self):
- if self.filtered(lambda holiday: holiday.state != 'draft'):
- raise UserError(_('Time off request must be in Draft state ("To Submit") in order to confirm it.'))
- self.write({'state': 'confirm'})
- holidays = self.filtered(lambda leave: leave.validation_type == 'no_validation')
- if holidays:
- # Automatic validation should be done in sudo, because user might not have the rights to do it by himself
- holidays.sudo().action_validate()
- self.activity_update()
- return True
- def action_approve(self):
- # if validation_type == 'both': this method is the first approval approval
- # if validation_type != 'both': this method calls action_validate() below
- if any(holiday.state != 'confirm' for holiday in self):
- raise UserError(_('Time off request must be confirmed ("To Approve") in order to approve it.'))
- current_employee = self.env.user.employee_id
- self.filtered(lambda hol: hol.validation_type == 'both').write({'state': 'validate1', 'first_approver_id': current_employee.id})
- # Post a second message, more verbose than the tracking message
- for holiday in self.filtered(lambda holiday: holiday.employee_id.user_id):
- user_tz = timezone(holiday.tz)
- utc_tz = pytz.utc.localize(holiday.date_from).astimezone(user_tz)
- holiday.message_post(
- body=_(
- 'Your %(leave_type)s planned on %(date)s has been accepted',
- leave_type=holiday.holiday_status_id.display_name,
- date=utc_tz.replace(tzinfo=None)
- ),
- partner_ids=holiday.employee_id.user_id.partner_id.ids)
- self.filtered(lambda hol: not hol.validation_type == 'both').action_validate()
- if not self.env.context.get('leave_fast_create'):
- self.activity_update()
- return True
- def _get_leaves_on_public_holiday(self):
- return self.filtered(lambda l: l.employee_id and not l.number_of_days)
- def _get_employees_from_holiday_type(self):
- self.ensure_one()
- if self.holiday_type == 'employee':
- employees = self.employee_ids
- elif self.holiday_type == 'category':
- employees = self.category_id.employee_ids
- elif self.holiday_type == 'company':
- employees = self.env['hr.employee'].search([('company_id', '=', self.mode_company_id.id)])
- else:
- employees = self.department_id.member_ids
- return employees
- def action_validate(self):
- current_employee = self.env.user.employee_id
- leaves = self._get_leaves_on_public_holiday()
- if leaves:
- raise ValidationError(_('The following employees are not supposed to work during that period:\n %s') % ','.join(leaves.mapped('employee_id.name')))
- if any(holiday.state not in ['confirm', 'validate1'] and holiday.validation_type != 'no_validation' for holiday in self):
- raise UserError(_('Time off request must be confirmed in order to approve it.'))
- self.write({'state': 'validate'})
- leaves_second_approver = self.env['hr.leave']
- leaves_first_approver = self.env['hr.leave']
- for leave in self:
- if leave.validation_type == 'both':
- leaves_second_approver += leave
- else:
- leaves_first_approver += leave
- if leave.holiday_type != 'employee' or\
- (leave.holiday_type == 'employee' and len(leave.employee_ids) > 1):
- employees = leave._get_employees_from_holiday_type()
- conflicting_leaves = self.env['hr.leave'].with_context(
- tracking_disable=True,
- mail_activity_automation_skip=True,
- leave_fast_create=True
- ).search([
- ('date_from', '<=', leave.date_to),
- ('date_to', '>', leave.date_from),
- ('state', 'not in', ['cancel', 'refuse']),
- ('holiday_type', '=', 'employee'),
- ('employee_id', 'in', employees.ids)])
- if conflicting_leaves:
- # YTI: More complex use cases could be managed in master
- if leave.leave_type_request_unit != 'day' or any(l.leave_type_request_unit == 'hour' for l in conflicting_leaves):
- raise ValidationError(_('You can not have 2 time off that overlaps on the same day.'))
- # keep track of conflicting leaves states before refusal
- target_states = {l.id: l.state for l in conflicting_leaves}
- conflicting_leaves.action_refuse()
- split_leaves_vals = []
- for conflicting_leave in conflicting_leaves:
- if conflicting_leave.leave_type_request_unit == 'half_day' and conflicting_leave.request_unit_half:
- continue
- # Leaves in days
- if conflicting_leave.date_from < leave.date_from:
- before_leave_vals = conflicting_leave.copy_data({
- 'date_from': conflicting_leave.date_from.date(),
- 'date_to': leave.date_from.date() + timedelta(days=-1),
- 'state': target_states[conflicting_leave.id],
- })[0]
- before_leave = self.env['hr.leave'].new(before_leave_vals)
- before_leave._compute_date_from_to()
- # Could happen for part-time contract, that time off is not necessary
- # anymore.
- # Imagine you work on monday-wednesday-friday only.
- # You take a time off on friday.
- # We create a company time off on friday.
- # By looking at the last attendance before the company time off
- # start date to compute the date_to, you would have a date_from > date_to.
- # Just don't create the leave at that time. That's the reason why we use
- # new instead of create. As the leave is not actually created yet, the sql
- # constraint didn't check date_from < date_to yet.
- if before_leave.date_from < before_leave.date_to:
- split_leaves_vals.append(before_leave._convert_to_write(before_leave._cache))
- if conflicting_leave.date_to > leave.date_to:
- after_leave_vals = conflicting_leave.copy_data({
- 'date_from': leave.date_to.date() + timedelta(days=1),
- 'date_to': conflicting_leave.date_to.date(),
- 'state': target_states[conflicting_leave.id],
- })[0]
- after_leave = self.env['hr.leave'].new(after_leave_vals)
- after_leave._compute_date_from_to()
- # Could happen for part-time contract, that time off is not necessary
- # anymore.
- if after_leave.date_from < after_leave.date_to:
- split_leaves_vals.append(after_leave._convert_to_write(after_leave._cache))
- split_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(split_leaves_vals)
- split_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request()
- values = leave._prepare_employees_holiday_values(employees)
- leaves = self.env['hr.leave'].with_context(
- tracking_disable=True,
- mail_activity_automation_skip=True,
- leave_fast_create=True,
- no_calendar_sync=True,
- leave_skip_state_check=True,
- # date_from and date_to are computed based on the employee tz
- # If _compute_date_from_to is used instead, it will trigger _compute_number_of_days
- # and create a conflict on the number of days calculation between the different leaves
- leave_compute_date_from_to=True,
- ).create(values)
- leaves._validate_leave_request()
- leaves_second_approver.write({'second_approver_id': current_employee.id})
- leaves_first_approver.write({'first_approver_id': current_employee.id})
- employee_requests = self.filtered(lambda hol: hol.holiday_type == 'employee')
- employee_requests._validate_leave_request()
- if not self.env.context.get('leave_fast_create'):
- employee_requests.filtered(lambda holiday: holiday.validation_type != 'no_validation').activity_update()
- return True
- def action_refuse(self):
- current_employee = self.env.user.employee_id
- if any(holiday.state not in ['draft', 'confirm', 'validate', 'validate1'] for holiday in self):
- raise UserError(_('Time off request must be confirmed or validated in order to refuse it.'))
- validated_holidays = self.filtered(lambda hol: hol.state == 'validate1')
- validated_holidays.write({'state': 'refuse', 'first_approver_id': current_employee.id})
- (self - validated_holidays).write({'state': 'refuse', 'second_approver_id': current_employee.id})
- # Delete the meeting
- self.mapped('meeting_id').write({'active': False})
- # If a category that created several holidays, cancel all related
- linked_requests = self.mapped('linked_request_ids')
- if linked_requests:
- linked_requests.action_refuse()
- # Post a second message, more verbose than the tracking message
- for holiday in self:
- if holiday.employee_id.user_id:
- holiday.message_post(
- body=_('Your %(leave_type)s planned on %(date)s has been refused', leave_type=holiday.holiday_status_id.display_name, date=holiday.date_from),
- partner_ids=holiday.employee_id.user_id.partner_id.ids)
- self.activity_update()
- return True
- def _action_user_cancel(self, reason):
- self.ensure_one()
- if not self.can_cancel:
- raise ValidationError(_('This time off cannot be canceled.'))
- self._force_cancel(reason, 'mail.mt_note')
- def _force_cancel(self, reason, msg_subtype='mail.mt_comment'):
- for leave in self:
- leave.message_post(
- body=_('The time off has been canceled: %s', reason),
- subtype_xmlid=msg_subtype
- )
- leave_sudo = self.sudo()
- leave_sudo.with_context(from_cancel_wizard=True).active = False
- leave_sudo.meeting_id.active = False
- leave_sudo._remove_resource_leave()
- def action_documents(self):
- domain = [('id', 'in', self.attachment_ids.ids)]
- return {
- 'name': _("Supporting Documents"),
- 'type': 'ir.actions.act_window',
- 'res_model': 'ir.attachment',
- 'context': {'create': False},
- 'view_mode': 'kanban',
- 'domain': domain
- }
- def _check_approval_update(self, state):
- """ Check if target state is achievable. """
- if self.env.is_superuser():
- return
- current_employee = self.env.user.employee_id
- is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
- is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
- for holiday in self:
- val_type = holiday.validation_type
- if not is_manager and state != 'confirm':
- if state == 'draft':
- if holiday.state == 'refuse':
- raise UserError(_('Only a Time Off Manager can reset a refused leave.'))
- if holiday.date_from and holiday.date_from.date() <= fields.Date.today():
- raise UserError(_('Only a Time Off Manager can reset a started leave.'))
- if holiday.employee_id != current_employee:
- raise UserError(_('Only a Time Off Manager can reset other people leaves.'))
- else:
- if val_type == 'no_validation' and current_employee == holiday.employee_id:
- continue
- # use ir.rule based first access check: department, members, ... (see security.xml)
- holiday.check_access_rule('write')
- # This handles states validate1 validate and refuse
- if holiday.employee_id == current_employee:
- raise UserError(_('Only a Time Off Manager can approve/refuse its own requests.'))
- if (state == 'validate1' and val_type == 'both') and holiday.holiday_type == 'employee':
- if not is_officer and self.env.user != holiday.employee_id.leave_manager_id:
- raise UserError(_('You must be either %s\'s manager or Time off Manager to approve this leave') % (holiday.employee_id.name))
- if (state == 'validate' and val_type == 'manager') and self.env.user != (holiday.employee_id | holiday.sudo().employee_ids).leave_manager_id:
- if holiday.employee_id:
- employees = holiday.employee_id
- else:
- employees = ', '.join(holiday.employee_ids.filtered(lambda e: e.leave_manager_id != self.env.user).mapped('name'))
- raise UserError(_('You must be %s\'s Manager to approve this leave', employees))
- if not is_officer and (state == 'validate' and val_type == 'hr') and holiday.holiday_type == 'employee':
- raise UserError(_('You must either be a Time off Officer or Time off Manager to approve this leave'))
- ###################################################
- # Leave modification methods
- ###################################################
- def _split_leave_on_gto(self, gto): #gto = global time off
- self.ensure_one()
- leave_start = date_utils.start_of(self.date_from, 'day')
- leave_end = date_utils.end_of(self.date_to - timedelta(seconds=1), 'day')
- gto_start = date_utils.start_of(gto['date_from'], 'day')
- gto_end = date_utils.end_of(gto['date_to'], 'day')
- leave_tz = timezone(self.employee_id.resource_id.tz)
- if gto_start <= leave_start\
- and gto_end > leave_start\
- and gto_end < leave_end:
- self.write({
- 'date_from': leave_tz.localize(gto_end + timedelta(seconds=1))\
- .astimezone(UTC).replace(tzinfo=None)
- })
- return self.env['hr.leave']
- if gto_start > leave_start\
- and gto_end < leave_end:
- copys = {
- 'date_from': self.date_from,
- 'date_to': leave_tz.localize(gto_start - timedelta(seconds=1))\
- .astimezone(UTC).replace(tzinfo=None)
- }
- self.write({
- 'date_from': leave_tz.localize(gto_end + timedelta(seconds=1))\
- .astimezone(UTC).replace(tzinfo=None)
- })
- return self.copy(copys)
- if gto_start > leave_start\
- and gto_start < leave_end\
- and gto_end >= leave_end:
- self.write({
- 'date_to': leave_tz.localize(gto_start - timedelta(seconds=1))\
- .astimezone(UTC).replace(tzinfo=None)
- })
- return self.env['hr.leave']
- def split_leave(self, time_domain_dict):
- self.ensure_one()
- new_leaves = self.env['hr.leave']
- for global_time_off in sorted(
- filter(lambda r: r['date_to'] > self.date_from and r['date_from'] < self.date_to, time_domain_dict),
- key=lambda r: r['date_from']):
- new_leave = self._split_leave_on_gto(global_time_off)
- if new_leave:
- new_leaves |= new_leave
- return new_leaves
- # ------------------------------------------------------------
- # Activity methods
- # ------------------------------------------------------------
- def _get_responsible_for_approval(self):
- self.ensure_one()
- responsible = self.env.user
- if self.holiday_type != 'employee':
- return responsible
- if self.validation_type == 'manager' or (self.validation_type == 'both' and self.state == 'confirm'):
- if self.employee_id.leave_manager_id:
- responsible = self.employee_id.leave_manager_id
- elif self.employee_id.parent_id.user_id:
- responsible = self.employee_id.parent_id.user_id
- elif self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
- if self.holiday_status_id.responsible_id:
- responsible = self.holiday_status_id.responsible_id
- return responsible
- def activity_update(self):
- to_clean, to_do = self.env['hr.leave'], self.env['hr.leave']
- for holiday in self:
- note = _(
- 'New %(leave_type)s Request created by %(user)s',
- leave_type=holiday.holiday_status_id.name,
- user=holiday.create_uid.name,
- )
- if holiday.state == 'draft':
- to_clean |= holiday
- elif holiday.state == 'confirm':
- holiday.activity_schedule(
- 'hr_holidays.mail_act_leave_approval',
- note=note,
- user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
- elif holiday.state == 'validate1':
- holiday.activity_feedback(['hr_holidays.mail_act_leave_approval'])
- holiday.activity_schedule(
- 'hr_holidays.mail_act_leave_second_approval',
- note=note,
- user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
- elif holiday.state == 'validate':
- to_do |= holiday
- elif holiday.state == 'refuse':
- to_clean |= holiday
- if to_clean:
- to_clean.activity_unlink(['hr_holidays.mail_act_leave_approval', 'hr_holidays.mail_act_leave_second_approval'])
- if to_do:
- to_do.activity_feedback(['hr_holidays.mail_act_leave_approval', 'hr_holidays.mail_act_leave_second_approval'])
- ####################################################
- # Messaging methods
- ####################################################
- def _notify_change(self, message, subtype_xmlid='mail.mt_note'):
- for leave in self:
- leave.message_post(body=message, subtype_xmlid=subtype_xmlid)
- recipient = None
- if leave.user_id:
- recipient = leave.user_id.partner_id.id
- elif leave.employee_id:
- recipient = leave.employee_id.address_home_id.id
- if recipient:
- self.env['mail.thread'].sudo().message_notify(
- body=message,
- partner_ids=[recipient]
- )
- def _track_subtype(self, init_values):
- if 'state' in init_values and self.state == 'validate':
- leave_notif_subtype = self.holiday_status_id.leave_notif_subtype_id
- return leave_notif_subtype or self.env.ref('hr_holidays.mt_leave')
- return super(HolidaysRequest, self)._track_subtype(init_values)
- def _notify_get_recipients_groups(self, msg_vals=None):
- """ Handle HR users and officers recipients that can validate or refuse holidays
- directly from email. """
- groups = super(HolidaysRequest, self)._notify_get_recipients_groups(msg_vals=msg_vals)
- if not self:
- return groups
- local_msg_vals = dict(msg_vals or {})
- self.ensure_one()
- hr_actions = []
- if self.state == 'confirm':
- app_action = self._notify_get_action_link('controller', controller='/leave/validate', **local_msg_vals)
- hr_actions += [{'url': app_action, 'title': _('Approve')}]
- if self.state in ['confirm', 'validate', 'validate1']:
- ref_action = self._notify_get_action_link('controller', controller='/leave/refuse', **local_msg_vals)
- hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
- holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
- new_group = (
- 'group_hr_holidays_user',
- lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
- {'actions': hr_actions}
- )
- return [new_group] + groups
- def message_subscribe(self, partner_ids=None, subtype_ids=None):
- # due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo
- if self.state in ['validate', 'validate1']:
- self.check_access_rights('read')
- self.check_access_rule('read')
- return super(HolidaysRequest, self.sudo()).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
- return super(HolidaysRequest, self).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
- @api.model
- def get_unusual_days(self, date_from, date_to=None):
- return self.env.user.employee_id.sudo(False)._get_unusual_days(date_from, date_to)
- def _get_start_or_end_from_attendance(self, hour, date, employee):
- hour = float_to_time(float(hour))
- holiday_tz = timezone(employee.tz or self.env.user.tz or 'UTC')
- return holiday_tz.localize(datetime.combine(date, hour)).astimezone(UTC).replace(tzinfo=None)
- def _get_attendances(self, employee, request_date_from, request_date_to):
- resource_calendar_id = employee.resource_calendar_id or self.env.company.resource_calendar_id
- domain = [('calendar_id', '=', resource_calendar_id.id), ('display_type', '=', False)]
- attendances = self.env['resource.calendar.attendance'].read_group(domain,
- ['ids:array_agg(id)', 'hour_from:min(hour_from)', 'hour_to:max(hour_to)',
- 'week_type', 'dayofweek', 'day_period'],
- ['week_type', 'dayofweek', 'day_period'], lazy=False)
- # Must be sorted by dayofweek ASC and day_period DESC
- attendances = sorted([DummyAttendance(group['hour_from'], group['hour_to'], group['dayofweek'], group['day_period'], group['week_type']) for group in attendances], key=lambda att: (att.dayofweek, att.day_period != 'morning'))
- default_value = DummyAttendance(0, 0, 0, 'morning', False)
- if resource_calendar_id.two_weeks_calendar:
- # find week type of start_date
- start_week_type = self.env['resource.calendar.attendance'].get_week_type(request_date_from)
- attendance_actual_week = [att for att in attendances if att.week_type is False or int(att.week_type) == start_week_type]
- attendance_actual_next_week = [att for att in attendances if att.week_type is False or int(att.week_type) != start_week_type]
- # First, add days of actual week coming after date_from
- attendance_filtred = [att for att in attendance_actual_week if int(att.dayofweek) >= request_date_from.weekday()]
- # Second, add days of the other type of week
- attendance_filtred += list(attendance_actual_next_week)
- # Third, add days of actual week (to consider days that we have remove first because they coming before date_from)
- attendance_filtred += list(attendance_actual_week)
- end_week_type = self.env['resource.calendar.attendance'].get_week_type(request_date_to)
- attendance_actual_week = [att for att in attendances if att.week_type is False or int(att.week_type) == end_week_type]
- attendance_actual_next_week = [att for att in attendances if att.week_type is False or int(att.week_type) != end_week_type]
- attendance_filtred_reversed = list(reversed([att for att in attendance_actual_week if int(att.dayofweek) <= request_date_to.weekday()]))
- attendance_filtred_reversed += list(reversed(attendance_actual_next_week))
- attendance_filtred_reversed += list(reversed(attendance_actual_week))
- # find first attendance coming after first_day
- attendance_from = attendance_filtred[0]
- # find last attendance coming before last_day
- attendance_to = attendance_filtred_reversed[0]
- else:
- # find first attendance coming after first_day
- attendance_from = next((att for att in attendances if int(att.dayofweek) >= request_date_from.weekday()), attendances[0] if attendances else default_value)
- # find last attendance coming before last_day
- attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= request_date_to.weekday()), attendances[-1] if attendances else default_value)
- return (attendance_from, attendance_to)
|