hr_leave_allocation.py 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. # Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
  4. from collections import defaultdict
  5. import logging
  6. from datetime import datetime, time
  7. from dateutil.relativedelta import relativedelta
  8. from odoo import api, fields, models
  9. from odoo.addons.resource.models.resource import HOURS_PER_DAY
  10. from odoo.addons.hr_holidays.models.hr_leave import get_employee_from_context
  11. from odoo.exceptions import AccessError, UserError, ValidationError
  12. from odoo.tools.translate import _
  13. from odoo.tools.float_utils import float_round
  14. from odoo.tools.date_utils import get_timedelta
  15. from odoo.osv import expression
  16. _logger = logging.getLogger(__name__)
  17. class HolidaysAllocation(models.Model):
  18. """ Allocation Requests Access specifications: similar to leave requests """
  19. _name = "hr.leave.allocation"
  20. _description = "Time Off Allocation"
  21. _order = "create_date desc"
  22. _inherit = ['mail.thread', 'mail.activity.mixin']
  23. _mail_post_access = 'read'
  24. def _default_holiday_status_id(self):
  25. if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
  26. domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes')]
  27. else:
  28. domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes'), ('employee_requests', '=', 'yes')]
  29. return self.env['hr.leave.type'].search(domain, limit=1)
  30. def _domain_holiday_status_id(self):
  31. if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
  32. return [('requires_allocation', '=', 'yes')]
  33. return [('employee_requests', '=', 'yes')]
  34. name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False)
  35. name_validity = fields.Char('Description with validity', compute='_compute_description_validity')
  36. active = fields.Boolean(default=True)
  37. private_name = fields.Char('Allocation Description', groups='hr_holidays.group_hr_holidays_user')
  38. state = fields.Selection([
  39. ('draft', 'To Submit'),
  40. ('cancel', 'Cancelled'),
  41. ('confirm', 'To Approve'),
  42. ('refuse', 'Refused'),
  43. ('validate', 'Approved')
  44. ], string='Status', readonly=True, tracking=True, copy=False, default='draft',
  45. help="The status is set to 'To Submit', when an allocation request is created." +
  46. "\nThe status is 'To Approve', when an allocation request is confirmed by user." +
  47. "\nThe status is 'Refused', when an allocation request is refused by manager." +
  48. "\nThe status is 'Approved', when an allocation request is approved by manager.")
  49. date_from = fields.Date('Start Date', index=True, copy=False, default=fields.Date.context_today,
  50. states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, tracking=True, required=True)
  51. date_to = fields.Date('End Date', copy=False, tracking=True,
  52. states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
  53. holiday_status_id = fields.Many2one(
  54. "hr.leave.type", compute='_compute_holiday_status_id', store=True, string="Time Off Type", required=True, readonly=False,
  55. states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]},
  56. domain=_domain_holiday_status_id,
  57. default=_default_holiday_status_id)
  58. employee_id = fields.Many2one(
  59. 'hr.employee', compute='_compute_from_employee_ids', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True,
  60. states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
  61. employee_company_id = fields.Many2one(related='employee_id.company_id', readonly=True, store=True)
  62. active_employee = fields.Boolean('Active Employee', related='employee_id.active', readonly=True)
  63. manager_id = fields.Many2one('hr.employee', compute='_compute_manager_id', store=True, string='Manager')
  64. notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
  65. # duration
  66. number_of_days = fields.Float(
  67. 'Number of Days', compute='_compute_from_holiday_status_id', store=True, readonly=False, tracking=True, default=1,
  68. help='Duration in days. Reference field to use when necessary.')
  69. number_of_days_display = fields.Float(
  70. 'Duration (days)', compute='_compute_number_of_days_display',
  71. states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
  72. help="If Accrual Allocation: Number of days allocated in addition to the ones you will get via the accrual' system.")
  73. number_of_hours_display = fields.Float(
  74. 'Duration (hours)', compute='_compute_number_of_hours_display',
  75. help="If Accrual Allocation: Number of hours allocated in addition to the ones you will get via the accrual' system.")
  76. duration_display = fields.Char('Allocated (Days/Hours)', compute='_compute_duration_display',
  77. help="Field allowing to see the allocation duration in days or hours depending on the type_request_unit")
  78. # details
  79. parent_id = fields.Many2one('hr.leave.allocation', string='Parent')
  80. linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests')
  81. approver_id = fields.Many2one(
  82. 'hr.employee', string='First Approval', readonly=True, copy=False,
  83. help='This area is automatically filled by the user who validates the allocation')
  84. validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True)
  85. can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')
  86. can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
  87. type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True)
  88. # mode
  89. holiday_type = fields.Selection([
  90. ('employee', 'By Employee'),
  91. ('company', 'By Company'),
  92. ('department', 'By Department'),
  93. ('category', 'By Employee Tag')],
  94. string='Allocation Mode', readonly=True, required=True, default='employee',
  95. states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
  96. help="Allow to create requests in batchs:\n- By Employee: for a specific employee"
  97. "\n- By Company: all employees of the specified company"
  98. "\n- By Department: all employees of the specified department"
  99. "\n- By Employee Tag: all employees of the specific employee group category")
  100. employee_ids = fields.Many2many(
  101. 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employees', readonly=False,
  102. states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
  103. multi_employee = fields.Boolean(
  104. compute='_compute_from_employee_ids', store=True,
  105. help='Holds whether this allocation concerns more than 1 employee')
  106. mode_company_id = fields.Many2one(
  107. 'res.company', compute='_compute_from_holiday_type', store=True, string='Company Mode', readonly=False,
  108. states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
  109. department_id = fields.Many2one(
  110. 'hr.department', compute='_compute_department_id', store=True, string='Department',
  111. states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
  112. category_id = fields.Many2one(
  113. 'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag', readonly=False,
  114. states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
  115. # accrual configuration
  116. lastcall = fields.Date("Date of the last accrual allocation", readonly=True, default=fields.Date.context_today)
  117. nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True)
  118. allocation_type = fields.Selection(
  119. [
  120. ('regular', 'Regular Allocation'),
  121. ('accrual', 'Accrual Allocation')
  122. ], string="Allocation Type", default="regular", required=True, readonly=True,
  123. states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
  124. is_officer = fields.Boolean(compute='_compute_is_officer')
  125. accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', compute="_compute_from_holiday_status_id", store=True, readonly=False, domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]", tracking=True)
  126. max_leaves = fields.Float(compute='_compute_leaves')
  127. leaves_taken = fields.Float(compute='_compute_leaves')
  128. taken_leave_ids = fields.One2many('hr.leave', 'holiday_allocation_id', domain="[('state', 'in', ['confirm', 'validate1', 'validate'])]")
  129. _sql_constraints = [
  130. ('type_value',
  131. "CHECK( (holiday_type='employee' AND (employee_id IS NOT NULL OR multi_employee IS TRUE)) or "
  132. "(holiday_type='category' AND category_id IS NOT NULL) or "
  133. "(holiday_type='department' AND department_id IS NOT NULL) or "
  134. "(holiday_type='company' AND mode_company_id IS NOT NULL))",
  135. "The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."),
  136. ('duration_check', "CHECK( ( number_of_days > 0 AND allocation_type='regular') or (allocation_type != 'regular'))", "The duration must be greater than 0."),
  137. ]
  138. # The compute does not get triggered without a depends on record creation
  139. # aka keep the 'useless' depends
  140. @api.depends_context('uid')
  141. @api.depends('allocation_type')
  142. def _compute_is_officer(self):
  143. self.is_officer = self.env.user.has_group("hr_holidays.group_hr_holidays_user")
  144. @api.depends_context('uid')
  145. def _compute_description(self):
  146. self.check_access_rights('read')
  147. self.check_access_rule('read')
  148. is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
  149. for allocation in self:
  150. if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user:
  151. allocation.name = allocation.sudo().private_name
  152. else:
  153. allocation.name = '*****'
  154. def _inverse_description(self):
  155. is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
  156. for allocation in self:
  157. if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user:
  158. allocation.sudo().private_name = allocation.name
  159. def _search_description(self, operator, value):
  160. is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
  161. domain = [('private_name', operator, value)]
  162. if not is_officer:
  163. domain = expression.AND([domain, [('employee_id.user_id', '=', self.env.user.id)]])
  164. allocations = self.sudo().search(domain)
  165. return [('id', 'in', allocations.ids)]
  166. @api.depends('name', 'date_from', 'date_to')
  167. def _compute_description_validity(self):
  168. for allocation in self:
  169. if allocation.date_to:
  170. name_validity = _("%s (from %s to %s)", allocation.name, allocation.date_from.strftime("%b %d %Y"), allocation.date_to.strftime("%b %d %Y"))
  171. else:
  172. name_validity = _("%s (from %s to No Limit)", allocation.name, allocation.date_from.strftime("%b %d %Y"))
  173. allocation.name_validity = name_validity
  174. @api.depends('employee_id', 'holiday_status_id', 'taken_leave_ids.number_of_days', 'taken_leave_ids.state')
  175. def _compute_leaves(self):
  176. employee_days_per_allocation = self.holiday_status_id.with_context(ignore_future=True)._get_employees_days_per_allocation(self.employee_id.ids)
  177. for allocation in self:
  178. allocation.max_leaves = allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days
  179. allocation.leaves_taken = employee_days_per_allocation[allocation.employee_id.id][allocation.holiday_status_id][allocation]['leaves_taken']
  180. @api.depends('number_of_days')
  181. def _compute_number_of_days_display(self):
  182. for allocation in self:
  183. allocation.number_of_days_display = allocation.number_of_days
  184. @api.depends('number_of_days', 'holiday_status_id', 'employee_id', 'holiday_type')
  185. def _compute_number_of_hours_display(self):
  186. for allocation in self:
  187. allocation_calendar = allocation.holiday_status_id.company_id.resource_calendar_id
  188. if allocation.holiday_type == 'employee':
  189. allocation_calendar = allocation.employee_id.sudo().resource_calendar_id
  190. allocation.number_of_hours_display = allocation.number_of_days * (allocation_calendar.hours_per_day or HOURS_PER_DAY)
  191. @api.depends('number_of_hours_display', 'number_of_days_display')
  192. def _compute_duration_display(self):
  193. for allocation in self:
  194. allocation.duration_display = '%g %s' % (
  195. (float_round(allocation.number_of_hours_display, precision_digits=2)
  196. if allocation.type_request_unit == 'hour'
  197. else float_round(allocation.number_of_days_display, precision_digits=2)),
  198. _('hours') if allocation.type_request_unit == 'hour' else _('days'))
  199. @api.depends('state', 'employee_id', 'department_id')
  200. def _compute_can_reset(self):
  201. for allocation in self:
  202. try:
  203. allocation._check_approval_update('draft')
  204. except (AccessError, UserError):
  205. allocation.can_reset = False
  206. else:
  207. allocation.can_reset = True
  208. @api.depends('state', 'employee_id', 'department_id')
  209. def _compute_can_approve(self):
  210. for allocation in self:
  211. try:
  212. if allocation.state == 'confirm' and allocation.validation_type != 'no':
  213. allocation._check_approval_update('validate')
  214. except (AccessError, UserError):
  215. allocation.can_approve = False
  216. else:
  217. allocation.can_approve = True
  218. @api.depends('employee_ids')
  219. def _compute_from_employee_ids(self):
  220. for allocation in self:
  221. if len(allocation.employee_ids) == 1:
  222. allocation.employee_id = allocation.employee_ids[0]._origin
  223. else:
  224. allocation.employee_id = False
  225. allocation.multi_employee = (len(allocation.employee_ids) > 1)
  226. @api.depends('holiday_type')
  227. def _compute_from_holiday_type(self):
  228. default_employee_ids = self.env['hr.employee'].browse(self.env.context.get('default_employee_id')) or self.env.user.employee_id
  229. for allocation in self:
  230. if allocation.holiday_type == 'employee':
  231. if not allocation.employee_ids:
  232. allocation.employee_ids = self.env.user.employee_id
  233. allocation.mode_company_id = False
  234. allocation.category_id = False
  235. elif allocation.holiday_type == 'company':
  236. allocation.employee_ids = False
  237. if not allocation.mode_company_id:
  238. allocation.mode_company_id = self.env.company
  239. allocation.category_id = False
  240. elif allocation.holiday_type == 'department':
  241. allocation.employee_ids = False
  242. allocation.mode_company_id = False
  243. allocation.category_id = False
  244. elif allocation.holiday_type == 'category':
  245. allocation.employee_ids = False
  246. allocation.mode_company_id = False
  247. else:
  248. allocation.employee_ids = default_employee_ids
  249. @api.depends('holiday_type', 'employee_id')
  250. def _compute_department_id(self):
  251. for allocation in self:
  252. if allocation.holiday_type == 'employee':
  253. allocation.department_id = allocation.employee_id.department_id
  254. elif allocation.holiday_type == 'department':
  255. if not allocation.department_id:
  256. allocation.department_id = self.env.user.employee_id.department_id
  257. elif allocation.holiday_type == 'category':
  258. allocation.department_id = False
  259. @api.depends('employee_id')
  260. def _compute_manager_id(self):
  261. for allocation in self:
  262. allocation.manager_id = allocation.employee_id and allocation.employee_id.parent_id
  263. @api.depends('accrual_plan_id')
  264. def _compute_holiday_status_id(self):
  265. default_holiday_status_id = None
  266. for holiday in self:
  267. if not holiday.holiday_status_id:
  268. if holiday.accrual_plan_id:
  269. holiday.holiday_status_id = holiday.accrual_plan_id.time_off_type_id
  270. else:
  271. if not default_holiday_status_id: # fetch when we need it
  272. default_holiday_status_id = self._default_holiday_status_id()
  273. holiday.holiday_status_id = default_holiday_status_id
  274. @api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display', 'date_to')
  275. def _compute_from_holiday_status_id(self):
  276. accrual_allocations = self.filtered(lambda alloc: alloc.allocation_type == 'accrual' and not alloc.accrual_plan_id and alloc.holiday_status_id)
  277. accruals_dict = {}
  278. if accrual_allocations:
  279. accruals_read_group = self.env['hr.leave.accrual.plan'].read_group(
  280. [('time_off_type_id', 'in', accrual_allocations.holiday_status_id.ids)],
  281. ['time_off_type_id', 'ids:array_agg(id)'],
  282. ['time_off_type_id'],
  283. )
  284. accruals_dict = {res['time_off_type_id'][0]: res['ids'] for res in accruals_read_group}
  285. for allocation in self:
  286. allocation.number_of_days = allocation.number_of_days_display
  287. if allocation.type_request_unit == 'hour':
  288. allocation.number_of_days = allocation.number_of_hours_display / \
  289. (allocation.employee_id.sudo().resource_calendar_id.hours_per_day \
  290. or allocation.holiday_status_id.company_id.resource_calendar_id.hours_per_day \
  291. or HOURS_PER_DAY)
  292. if allocation.accrual_plan_id.time_off_type_id.id not in (False, allocation.holiday_status_id.id):
  293. allocation.accrual_plan_id = False
  294. if allocation.allocation_type == 'accrual' and not allocation.accrual_plan_id:
  295. if allocation.holiday_status_id:
  296. allocation.accrual_plan_id = accruals_dict.get(allocation.holiday_status_id.id, [False])[0]
  297. def _end_of_year_accrual(self):
  298. # to override in payroll
  299. today = fields.Date.today()
  300. last_day_last_year = today + relativedelta(years=-1, month=12, day=31)
  301. first_day_this_year = today + relativedelta(month=1, day=1)
  302. for allocation in self:
  303. current_level = allocation._get_current_accrual_plan_level_id(first_day_this_year)[0]
  304. if not current_level:
  305. continue
  306. # lastcall has two cases:
  307. # 1. The period was fully ran until the last day of last year
  308. # 2. The period was not fully ran until the last day of last year
  309. # For case 2, we need to prorata the number of days so need to check if the lastcall within the current level period
  310. lastcall = current_level._get_previous_date(last_day_last_year) if allocation.lastcall < current_level._get_previous_date(last_day_last_year) else allocation.lastcall
  311. nextcall = current_level._get_next_date(last_day_last_year)
  312. if current_level.action_with_unused_accruals == 'lost':
  313. # Allocations are lost but number_of_days should not be lower than leaves_taken
  314. allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': lastcall, 'nextcall': nextcall})
  315. elif current_level.action_with_unused_accruals == 'postponed' and current_level.postpone_max_days:
  316. # Make sure the period was ran until the last day of last year
  317. if allocation.nextcall:
  318. allocation.nextcall = first_day_this_year
  319. # date_to should be first day of this year so the prorata amount is computed correctly
  320. allocation._process_accrual_plans(first_day_this_year, True)
  321. number_of_days = min(allocation.number_of_days - allocation.leaves_taken, current_level.postpone_max_days) + allocation.leaves_taken
  322. allocation.write({'number_of_days': number_of_days, 'lastcall': lastcall, 'nextcall': nextcall})
  323. def _get_current_accrual_plan_level_id(self, date, level_ids=False):
  324. """
  325. Returns a pair (accrual_plan_level, idx) where accrual_plan_level is the level for the given date
  326. and idx is the index for the plan in the ordered set of levels
  327. """
  328. self.ensure_one()
  329. if not self.accrual_plan_id.level_ids:
  330. return (False, False)
  331. # Sort by sequence which should be equivalent to the level
  332. if not level_ids:
  333. level_ids = self.accrual_plan_id.level_ids.sorted('sequence')
  334. current_level = False
  335. current_level_idx = -1
  336. for idx, level in enumerate(level_ids):
  337. if date > self.date_from + get_timedelta(level.start_count, level.start_type):
  338. current_level = level
  339. current_level_idx = idx
  340. # If transition_mode is set to `immediately` or we are currently on the first level
  341. # the current_level is simply the first level in the list.
  342. if current_level_idx <= 0 or self.accrual_plan_id.transition_mode == "immediately":
  343. return (current_level, current_level_idx)
  344. # In this case we have to verify that the 'previous level' is not the current one due to `end_of_accrual`
  345. level_start_date = self.date_from + get_timedelta(current_level.start_count, current_level.start_type)
  346. previous_level = level_ids[current_level_idx - 1]
  347. # If the next date from the current level's start date is before the last call of the previous level
  348. # return the previous level
  349. if current_level._get_next_date(level_start_date) < previous_level._get_next_date(level_start_date):
  350. return (previous_level, current_level_idx - 1)
  351. return (current_level, current_level_idx)
  352. def _process_accrual_plan_level(self, level, start_period, start_date, end_period, end_date):
  353. """
  354. Returns the added days for that level
  355. """
  356. self.ensure_one()
  357. if level.is_based_on_worked_time:
  358. start_dt = datetime.combine(start_date, datetime.min.time())
  359. end_dt = datetime.combine(end_date, datetime.min.time())
  360. worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
  361. [self.employee_id.id]['hours']
  362. if start_period != start_date or end_period != end_date:
  363. start_dt = datetime.combine(start_period, datetime.min.time())
  364. end_dt = datetime.combine(end_period, datetime.min.time())
  365. planned_worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
  366. [self.employee_id.id]['hours']
  367. else:
  368. planned_worked = worked
  369. left = self.employee_id.sudo()._get_leave_days_data_batch(start_dt, end_dt,
  370. domain=[('time_type', '=', 'leave')])[self.employee_id.id]['hours']
  371. work_entry_prorata = worked / (left + planned_worked) if (left + planned_worked) else 0
  372. added_value = work_entry_prorata * level.added_value
  373. else:
  374. added_value = level.added_value
  375. # Convert time in hours to time in days in case the level is encoded in hours
  376. if level.added_value_type == 'hours':
  377. added_value = added_value / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
  378. period_prorata = 1
  379. if (start_period != start_date or end_period != end_date) and not level.is_based_on_worked_time:
  380. period_days = (end_period - start_period)
  381. call_days = (end_date - start_date)
  382. period_prorata = min(1, call_days / period_days) if period_days else 1
  383. return added_value * period_prorata
  384. def _process_accrual_plans(self, date_to=False, force_period=False):
  385. """
  386. This method is part of the cron's process.
  387. The goal of this method is to retroactively apply accrual plan levels and progress from nextcall to date_to or today.
  388. If force_period is set, the accrual will run until date_to in a prorated way (used for end of year accrual actions).
  389. """
  390. date_to = date_to or fields.Date.today()
  391. first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, cancel and create a new one.""")
  392. for allocation in self:
  393. level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
  394. if not level_ids:
  395. continue
  396. if not allocation.nextcall:
  397. first_level = level_ids[0]
  398. first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
  399. if date_to < first_level_start_date:
  400. # Accrual plan is not configured properly or has not started
  401. continue
  402. allocation.lastcall = max(allocation.lastcall, first_level_start_date)
  403. allocation.nextcall = first_level._get_next_date(allocation.lastcall)
  404. if len(level_ids) > 1:
  405. second_level_start_date = allocation.date_from + get_timedelta(level_ids[1].start_count, level_ids[1].start_type)
  406. allocation.nextcall = min(second_level_start_date, allocation.nextcall)
  407. allocation._message_log(body=first_allocation)
  408. days_added_per_level = defaultdict(lambda: 0)
  409. while allocation.nextcall <= date_to:
  410. (current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
  411. if not current_level:
  412. break
  413. current_level_maximum_leave = current_level.maximum_leave if current_level.added_value_type == "days" else current_level.maximum_leave / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
  414. nextcall = current_level._get_next_date(allocation.nextcall)
  415. # Since _get_previous_date returns the given date if it corresponds to a call date
  416. # this will always return lastcall except possibly on the first call
  417. # this is used to prorate the first number of days given to the employee
  418. period_start = current_level._get_previous_date(allocation.lastcall)
  419. period_end = current_level._get_next_date(allocation.lastcall)
  420. # Also prorate this accrual in the event that we are passing from one level to another
  421. if current_level_idx < (len(level_ids) - 1) and allocation.accrual_plan_id.transition_mode == 'immediately':
  422. next_level = level_ids[current_level_idx + 1]
  423. current_level_last_date = allocation.date_from + get_timedelta(next_level.start_count, next_level.start_type)
  424. if allocation.nextcall != current_level_last_date:
  425. nextcall = min(nextcall, current_level_last_date)
  426. # We have to check for end of year actions if it is within our period
  427. # since we can create retroactive allocations.
  428. if allocation.lastcall.year < allocation.nextcall.year and\
  429. current_level.action_with_unused_accruals == 'postponed' and\
  430. current_level.postpone_max_days > 0:
  431. # Compute number of days kept
  432. allocation_days = allocation.number_of_days - allocation.leaves_taken
  433. allowed_to_keep = max(0, current_level.postpone_max_days - allocation_days)
  434. number_of_days = min(allocation_days, current_level.postpone_max_days)
  435. allocation.number_of_days = number_of_days + allocation.leaves_taken
  436. total_gained_days = sum(days_added_per_level.values())
  437. days_added_per_level.clear()
  438. days_added_per_level[current_level] = min(total_gained_days, allowed_to_keep)
  439. gained_days = allocation._process_accrual_plan_level(
  440. current_level, period_start, allocation.lastcall, period_end, allocation.nextcall)
  441. days_added_per_level[current_level] += gained_days
  442. if current_level_maximum_leave > 0 and sum(days_added_per_level.values()) > current_level_maximum_leave:
  443. days_added_per_level[current_level] -= sum(days_added_per_level.values()) - current_level_maximum_leave
  444. allocation.lastcall = allocation.nextcall
  445. allocation.nextcall = nextcall
  446. if force_period and allocation.nextcall > date_to:
  447. allocation.nextcall = date_to
  448. force_period = False
  449. if days_added_per_level:
  450. number_of_days_to_add = allocation.number_of_days + sum(days_added_per_level.values())
  451. max_allocation_days = current_level_maximum_leave + (allocation.leaves_taken if allocation.type_request_unit != "hour" else allocation.leaves_taken / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY))
  452. # Let's assume the limit of the last level is the correct one
  453. allocation.number_of_days = min(number_of_days_to_add, max_allocation_days) if current_level_maximum_leave > 0 else number_of_days_to_add
  454. @api.model
  455. def _update_accrual(self):
  456. """
  457. Method called by the cron task in order to increment the number_of_days when
  458. necessary.
  459. """
  460. # Get the current date to determine the start and end of the accrual period
  461. today = datetime.combine(fields.Date.today(), time(0, 0, 0))
  462. this_year_first_day = (today + relativedelta(day=1, month=1)).date()
  463. end_of_year_allocations = self.search(
  464. [('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
  465. '|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()), ('lastcall', '<', this_year_first_day)])
  466. end_of_year_allocations._end_of_year_accrual()
  467. end_of_year_allocations.flush_model()
  468. allocations = self.search(
  469. [('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
  470. '|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()),
  471. '|', ('nextcall', '=', False), ('nextcall', '<=', today)])
  472. allocations._process_accrual_plans()
  473. ####################################################
  474. # ORM Overrides methods
  475. ####################################################
  476. def onchange(self, values, field_name, field_onchange):
  477. # Try to force the leave_type name_get when creating new records
  478. # This is called right after pressing create and returns the name_get for
  479. # most fields in the view.
  480. if field_onchange.get('employee_id') and 'employee_id' not in self._context and values:
  481. employee_id = get_employee_from_context(values, self._context, self.env.user.employee_id.id)
  482. self = self.with_context(employee_id=employee_id)
  483. return super().onchange(values, field_name, field_onchange)
  484. def name_get(self):
  485. res = []
  486. for allocation in self:
  487. if allocation.holiday_type == 'company':
  488. target = allocation.mode_company_id.name
  489. elif allocation.holiday_type == 'department':
  490. target = allocation.department_id.name
  491. elif allocation.holiday_type == 'category':
  492. target = allocation.category_id.name
  493. elif allocation.employee_id:
  494. target = allocation.employee_id.name
  495. else:
  496. target = ', '.join(allocation.employee_ids.sudo().mapped('name'))
  497. res.append(
  498. (allocation.id,
  499. _("Allocation of %(allocation_name)s : %(duration).2f %(duration_type)s to %(person)s",
  500. allocation_name=allocation.holiday_status_id.sudo().name,
  501. duration=allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days,
  502. duration_type=_('hours') if allocation.type_request_unit == 'hour' else _('days'),
  503. person=target
  504. ))
  505. )
  506. return res
  507. def add_follower(self, employee_id):
  508. employee = self.env['hr.employee'].browse(employee_id)
  509. if employee.user_id:
  510. self.message_subscribe(partner_ids=employee.user_id.partner_id.ids)
  511. @api.model_create_multi
  512. def create(self, vals_list):
  513. """ Override to avoid automatic logging of creation """
  514. for values in vals_list:
  515. if 'state' in values and values['state'] not in ('draft', 'confirm'):
  516. raise UserError(_('Incorrect state for new allocation'))
  517. employee_id = values.get('employee_id', False)
  518. if not values.get('department_id'):
  519. values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
  520. # default `lastcall` to `nextcall`
  521. if 'date_from' in values and 'lastcall' not in values:
  522. values['lastcall'] = values['date_from']
  523. holidays = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
  524. for holiday in holidays:
  525. partners_to_subscribe = set()
  526. if holiday.employee_id.user_id:
  527. partners_to_subscribe.add(holiday.employee_id.user_id.partner_id.id)
  528. if holiday.validation_type == 'officer':
  529. partners_to_subscribe.add(holiday.employee_id.parent_id.user_id.partner_id.id)
  530. partners_to_subscribe.add(holiday.employee_id.leave_manager_id.partner_id.id)
  531. holiday.message_subscribe(partner_ids=tuple(partners_to_subscribe))
  532. if not self._context.get('import_file'):
  533. holiday.activity_update()
  534. if holiday.validation_type == 'no' and holiday.state == 'draft':
  535. holiday.action_confirm()
  536. return holidays
  537. def write(self, values):
  538. if not self.env.context.get('toggle_active') and not bool(values.get('active', True)):
  539. if any(allocation.state not in ['draft', 'cancel', 'refuse'] for allocation in self):
  540. raise UserError(_('You cannot archive an allocation which is in confirm or validate state.'))
  541. employee_id = values.get('employee_id', False)
  542. if values.get('state'):
  543. self._check_approval_update(values['state'])
  544. result = super(HolidaysAllocation, self).write(values)
  545. self.add_follower(employee_id)
  546. return result
  547. @api.ondelete(at_uninstall=False)
  548. def _unlink_if_correct_states(self):
  549. state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
  550. for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']):
  551. raise UserError(_('You cannot delete an allocation request which is in %s state.') % (state_description_values.get(holiday.state),))
  552. @api.ondelete(at_uninstall=False)
  553. def _unlink_if_no_leaves(self):
  554. if any(allocation.holiday_status_id.requires_allocation == 'yes' and allocation.leaves_taken > 0 for allocation in self):
  555. raise UserError(_('You cannot delete an allocation request which has some validated leaves.'))
  556. def _get_mail_redirect_suggested_company(self):
  557. return self.holiday_status_id.company_id
  558. ####################################################
  559. # Business methods
  560. ####################################################
  561. def _prepare_holiday_values(self, employees):
  562. self.ensure_one()
  563. return [{
  564. 'name': self.name,
  565. 'holiday_type': 'employee',
  566. 'holiday_status_id': self.holiday_status_id.id,
  567. 'notes': self.notes,
  568. 'number_of_days': self.number_of_days,
  569. 'parent_id': self.id,
  570. 'employee_id': employee.id,
  571. 'employee_ids': [(6, 0, [employee.id])],
  572. 'state': 'confirm',
  573. 'allocation_type': self.allocation_type,
  574. 'date_from': self.date_from,
  575. 'date_to': self.date_to,
  576. 'accrual_plan_id': self.accrual_plan_id.id,
  577. } for employee in employees]
  578. def action_draft(self):
  579. if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
  580. raise UserError(_('Allocation request state must be "Refused" or "To Approve" in order to be reset to Draft.'))
  581. self.write({
  582. 'state': 'draft',
  583. 'approver_id': False,
  584. })
  585. linked_requests = self.mapped('linked_request_ids')
  586. if linked_requests:
  587. linked_requests.action_draft()
  588. linked_requests.unlink()
  589. self.activity_update()
  590. return True
  591. def action_confirm(self):
  592. if self.filtered(lambda holiday: holiday.state != 'draft' and holiday.validation_type != 'no'):
  593. raise UserError(_('Allocation request must be in Draft state ("To Submit") in order to confirm it.'))
  594. validated_holidays = self.filtered(lambda holiday: holiday.state == 'validate')
  595. res = (self - validated_holidays).write({'state': 'confirm'})
  596. self.activity_update()
  597. no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
  598. self.filtered(lambda holiday: (holiday.id in no_employee_requests or holiday.validation_type == 'no') and holiday.state != 'validate').action_validate()
  599. return res
  600. def action_validate(self):
  601. current_employee = self.env.user.employee_id
  602. no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
  603. if any((holiday.state != 'confirm' and holiday.id not in no_employee_requests and holiday.validation_type != 'no') for holiday in self):
  604. raise UserError(_('Allocation request must be confirmed in order to approve it.'))
  605. self.write({
  606. 'state': 'validate',
  607. 'approver_id': current_employee.id
  608. })
  609. for holiday in self:
  610. holiday._action_validate_create_childs()
  611. self.activity_update()
  612. return True
  613. def _action_validate_create_childs(self):
  614. childs = self.env['hr.leave.allocation']
  615. # In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation
  616. # Otherwise we do need to create an allocation for all employees to have a behaviour that is in line
  617. # with the other holiday_type
  618. if self.state == 'validate' and (self.holiday_type in ['category', 'department', 'company'] or
  619. (self.holiday_type == 'employee' and len(self.employee_ids) > 1)):
  620. if self.holiday_type == 'employee':
  621. employees = self.employee_ids
  622. elif self.holiday_type == 'category':
  623. employees = self.category_id.employee_ids
  624. elif self.holiday_type == 'department':
  625. employees = self.department_id.member_ids
  626. else:
  627. employees = self.env['hr.employee'].search([('company_id', '=', self.mode_company_id.id)])
  628. allocation_create_vals = self._prepare_holiday_values(employees)
  629. childs += self.with_context(
  630. mail_notify_force_send=False,
  631. mail_activity_automation_skip=True
  632. ).create(allocation_create_vals)
  633. if childs:
  634. childs.action_validate()
  635. return childs
  636. def action_refuse(self):
  637. current_employee = self.env.user.employee_id
  638. if any(holiday.state not in ['confirm', 'validate', 'validate1'] for holiday in self):
  639. raise UserError(_('Allocation request must be confirmed or validated in order to refuse it.'))
  640. self.write({'state': 'refuse', 'approver_id': current_employee.id})
  641. # If a category that created several holidays, cancel all related
  642. linked_requests = self.mapped('linked_request_ids')
  643. if linked_requests:
  644. linked_requests.action_refuse()
  645. self.activity_update()
  646. return True
  647. def _check_approval_update(self, state):
  648. """ Check if target state is achievable. """
  649. if self.env.is_superuser():
  650. return
  651. current_employee = self.env.user.employee_id
  652. if not current_employee:
  653. return
  654. is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
  655. is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
  656. for holiday in self:
  657. val_type = holiday.holiday_status_id.sudo().allocation_validation_type
  658. if state == 'confirm':
  659. continue
  660. if state == 'draft':
  661. if holiday.employee_id != current_employee and not is_manager:
  662. raise UserError(_('Only a time off Manager can reset other people allocation.'))
  663. continue
  664. if not is_officer and self.env.user != holiday.employee_id.leave_manager_id and not val_type == 'no':
  665. raise UserError(_('Only a time off Officer/Responsible or Manager can approve or refuse time off requests.'))
  666. if is_officer or self.env.user == holiday.employee_id.leave_manager_id:
  667. # use ir.rule based first access check: department, members, ... (see security.xml)
  668. holiday.check_access_rule('write')
  669. if holiday.employee_id == current_employee and not is_manager and not val_type == 'no':
  670. raise UserError(_('Only a time off Manager can approve its own requests.'))
  671. @api.onchange('allocation_type')
  672. def _onchange_allocation_type(self):
  673. if self.allocation_type == 'accrual':
  674. self.number_of_days = 0.0
  675. elif not self.number_of_days_display:
  676. self.number_of_days = 1.0
  677. # ------------------------------------------------------------
  678. # Activity methods
  679. # ------------------------------------------------------------
  680. def _get_responsible_for_approval(self):
  681. self.ensure_one()
  682. responsible = self.env.user
  683. if self.validation_type == 'officer' or self.validation_type == 'set':
  684. if self.holiday_status_id.responsible_id:
  685. responsible = self.holiday_status_id.responsible_id
  686. return responsible
  687. def activity_update(self):
  688. to_clean, to_do = self.env['hr.leave.allocation'], self.env['hr.leave.allocation']
  689. for allocation in self:
  690. if allocation.validation_type != 'no':
  691. note = _(
  692. 'New Allocation Request created by %(user)s: %(count)s Days of %(allocation_type)s',
  693. user=allocation.create_uid.name,
  694. count=allocation.number_of_days,
  695. allocation_type=allocation.holiday_status_id.name
  696. )
  697. if allocation.state == 'draft':
  698. to_clean |= allocation
  699. elif allocation.state == 'confirm':
  700. allocation.activity_schedule(
  701. 'hr_holidays.mail_act_leave_allocation_approval',
  702. note=note,
  703. user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
  704. elif allocation.state == 'validate1':
  705. allocation.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval'])
  706. allocation.activity_schedule(
  707. 'hr_holidays.mail_act_leave_allocation_second_approval',
  708. note=note,
  709. user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
  710. elif allocation.state == 'validate':
  711. to_do |= allocation
  712. elif allocation.state == 'refuse':
  713. to_clean |= allocation
  714. if to_clean:
  715. to_clean.activity_unlink(['hr_holidays.mail_act_leave_allocation_approval', 'hr_holidays.mail_act_leave_allocation_second_approval'])
  716. if to_do:
  717. to_do.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval', 'hr_holidays.mail_act_leave_allocation_second_approval'])
  718. ####################################################
  719. # Messaging methods
  720. ####################################################
  721. def _track_subtype(self, init_values):
  722. if 'state' in init_values and self.state == 'validate':
  723. allocation_notif_subtype_id = self.holiday_status_id.allocation_notif_subtype_id
  724. return allocation_notif_subtype_id or self.env.ref('hr_holidays.mt_leave_allocation')
  725. return super(HolidaysAllocation, self)._track_subtype(init_values)
  726. def _notify_get_recipients_groups(self, msg_vals=None):
  727. """ Handle HR users and officers recipients that can validate or refuse holidays
  728. directly from email. """
  729. groups = super(HolidaysAllocation, self)._notify_get_recipients_groups(msg_vals=msg_vals)
  730. if not self:
  731. return groups
  732. local_msg_vals = dict(msg_vals or {})
  733. self.ensure_one()
  734. hr_actions = []
  735. if self.state == 'confirm':
  736. app_action = self._notify_get_action_link('controller', controller='/allocation/validate', **local_msg_vals)
  737. hr_actions += [{'url': app_action, 'title': _('Approve')}]
  738. if self.state in ['confirm', 'validate', 'validate1']:
  739. ref_action = self._notify_get_action_link('controller', controller='/allocation/refuse', **local_msg_vals)
  740. hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
  741. holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
  742. new_group = (
  743. 'group_hr_holidays_user',
  744. lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
  745. {'actions': hr_actions}
  746. )
  747. return [new_group] + groups
  748. def message_subscribe(self, partner_ids=None, subtype_ids=None):
  749. # due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo
  750. if self.state in ['validate', 'validate1']:
  751. self.check_access_rights('read')
  752. self.check_access_rule('read')
  753. return super(HolidaysAllocation, self.sudo()).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
  754. return super(HolidaysAllocation, self).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)