mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-24 22:52:07 +02:00
19.0 vanilla
This commit is contained in:
parent
e1d89e11e3
commit
a1f02d8cc7
225 changed files with 2335 additions and 775 deletions
|
|
@ -11,7 +11,7 @@ msgstr ""
|
|||
"Project-Id-Version: Odoo Server saas~18.4\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-25 18:35+0000\n"
|
||||
"PO-Revision-Date: 2026-03-06 17:49+0000\n"
|
||||
"PO-Revision-Date: 2026-03-14 09:44+0000\n"
|
||||
"Last-Translator: \"Fernanda Alvarez (mfar)\" <mfar@odoo.com>\n"
|
||||
"Language-Team: Spanish (Latin America) <https://translate.odoo.com/projects/"
|
||||
"odoo-19/hr_holidays/es_419/>\n"
|
||||
|
|
@ -20,7 +20,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-python
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
# Martin Trigaux, 2023
|
||||
# Jolien De Paepe, 2023
|
||||
# "Dylan Kiss (dyki)" <dyki@odoo.com>, 2025.
|
||||
# "Manon Rondou (ronm)" <ronm@odoo.com>, 2025.
|
||||
# "Manon Rondou (ronm)" <ronm@odoo.com>, 2025, 2026.
|
||||
# Weblate <noreply-mt-weblate@weblate.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-25 18:35+0000\n"
|
||||
"PO-Revision-Date: 2025-11-17 17:21+0000\n"
|
||||
"PO-Revision-Date: 2026-03-14 09:33+0000\n"
|
||||
"Last-Translator: \"Manon Rondou (ronm)\" <ronm@odoo.com>\n"
|
||||
"Language-Team: French <https://translate.odoo.com/projects/odoo-19/"
|
||||
"hr_holidays/fr/>\n"
|
||||
|
|
@ -25,7 +25,7 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : ((n != 0 && n % "
|
||||
"1000000 == 0) ? 1 : 2);\n"
|
||||
"X-Generator: Weblate 5.12.2\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-python
|
||||
|
|
@ -698,7 +698,7 @@ msgstr "Activités de"
|
|||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave__activity_exception_decoration
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave_allocation__activity_exception_decoration
|
||||
msgid "Activity Exception Decoration"
|
||||
msgstr "Activité exception décoration"
|
||||
msgstr "Indicateur d’exception d’activité"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave__activity_state
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ msgstr ""
|
|||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-25 18:35+0000\n"
|
||||
"PO-Revision-Date: 2026-03-06 17:45+0000\n"
|
||||
"PO-Revision-Date: 2026-03-17 22:06+0000\n"
|
||||
"Last-Translator: Bren Driesen <brdri@odoo.com>\n"
|
||||
"Language-Team: Dutch <https://translate.odoo.com/projects/odoo-19/"
|
||||
"hr_holidays/nl/>\n"
|
||||
|
|
@ -23,7 +23,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-python
|
||||
|
|
@ -2350,7 +2350,7 @@ msgstr "Hoeveel tijd kan worden overgedragen :"
|
|||
#. module: hr_holidays
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_employee__hr_icon_display
|
||||
msgid "Hr Icon Display"
|
||||
msgstr "HR-pictogramweergave"
|
||||
msgstr "HR-icoonweergave"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_calendar_event__id
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ msgstr ""
|
|||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-25 18:35+0000\n"
|
||||
"PO-Revision-Date: 2026-03-06 17:52+0000\n"
|
||||
"PO-Revision-Date: 2026-03-14 20:05+0000\n"
|
||||
"Last-Translator: Tomáš Píšek <Tomas.Pisek@seznam.cz>\n"
|
||||
"Language-Team: Slovak <https://translate.odoo.com/projects/odoo-19/"
|
||||
"hr_holidays/sk/>\n"
|
||||
|
|
@ -31,7 +31,7 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && "
|
||||
"n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-python
|
||||
|
|
@ -1229,7 +1229,7 @@ msgstr ""
|
|||
#: model_terms:ir.ui.view,arch_db:hr_holidays.hr_leave_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_hr_holidays_summary_employee
|
||||
msgid "Cancel"
|
||||
msgstr "Zrušené"
|
||||
msgstr "Zrušiť"
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-javascript
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ msgstr ""
|
|||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-25 18:35+0000\n"
|
||||
"PO-Revision-Date: 2026-03-06 17:45+0000\n"
|
||||
"PO-Revision-Date: 2026-03-20 17:34+0000\n"
|
||||
"Last-Translator: Hanna Kharraziha <hakha@odoo.com>\n"
|
||||
"Language-Team: Swedish <https://translate.odoo.com/projects/odoo-19/"
|
||||
"hr_holidays/sv/>\n"
|
||||
|
|
@ -37,7 +37,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.16.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-python
|
||||
|
|
@ -676,7 +676,7 @@ msgstr "Aktivitet undantaget dekoration"
|
|||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave__activity_state
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave_allocation__activity_state
|
||||
msgid "Activity State"
|
||||
msgstr "Aktivitetstillstånd"
|
||||
msgstr "Aktivitetsstatus"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model,name:hr_holidays.model_mail_activity_type
|
||||
|
|
@ -2099,7 +2099,7 @@ msgstr "Från datum"
|
|||
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_hr_holidays_filter
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_hr_leave_allocation_filter
|
||||
msgid "Future Activities"
|
||||
msgstr "Framtida aktiviteter"
|
||||
msgstr "Kommande aktiviteter"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays.hr_leave_generate_multi_wizard_view_form
|
||||
|
|
@ -2592,7 +2592,7 @@ msgstr "Senast uppdaterad den"
|
|||
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_hr_holidays_filter
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_hr_leave_allocation_filter
|
||||
msgid "Late Activities"
|
||||
msgstr "Sena aktiviteter"
|
||||
msgstr "Försenade aktiviteter"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave_report_calendar__leave_id
|
||||
|
|
@ -2774,7 +2774,7 @@ msgstr "Medlem i avdelningen"
|
|||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave__message_has_error
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave_allocation__message_has_error
|
||||
msgid "Message Delivery error"
|
||||
msgstr "Meddelande gick inte att skicka"
|
||||
msgstr "Leveransfelmeddelande"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model,name:hr_holidays.model_mail_message_subtype
|
||||
|
|
@ -2846,7 +2846,7 @@ msgstr "Flera förfrågningar"
|
|||
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_hr_holidays_filter
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_hr_leave_allocation_filter
|
||||
msgid "My Activities"
|
||||
msgstr "Mina aktivieteter"
|
||||
msgstr "Mina aktiviteter"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave__my_activity_date_deadline
|
||||
|
|
@ -3155,7 +3155,7 @@ msgstr "Antal meddelanden som kräver en åtgärd"
|
|||
#: model:ir.model.fields,help:hr_holidays.field_hr_leave__message_has_error_counter
|
||||
#: model:ir.model.fields,help:hr_holidays.field_hr_leave_allocation__message_has_error_counter
|
||||
msgid "Number of messages with delivery error"
|
||||
msgstr "Antal meddelanden som inte kunde skickas"
|
||||
msgstr "Antal meddelanden med leveransfel"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model.fields.selection,name:hr_holidays.selection__hr_leave_accrual_level__second_month__10
|
||||
|
|
@ -3927,8 +3927,8 @@ msgid ""
|
|||
"The ISO country code in two chars. \n"
|
||||
"You can use this field for quick search."
|
||||
msgstr ""
|
||||
"ISO-landskoden med två tecken.\n"
|
||||
"Du kan använda det här fältet för snabbsökning."
|
||||
"ISO-kod med två tecken.\n"
|
||||
"Fältet används för snabbsökning."
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-python
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@
|
|||
# Martin Trigaux, 2023
|
||||
# Thi Huong Nguyen, 2023
|
||||
# "Dylan Kiss (dyki)" <dyki@odoo.com>, 2025.
|
||||
# "Thi Huong Nguyen (thng)" <thng@odoo.com>, 2025.
|
||||
# "Thi Huong Nguyen (thng)" <thng@odoo.com>, 2025, 2026.
|
||||
# Weblate <noreply-mt-weblate@weblate.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-25 18:35+0000\n"
|
||||
"PO-Revision-Date: 2025-12-02 14:48+0000\n"
|
||||
"PO-Revision-Date: 2026-03-20 09:45+0000\n"
|
||||
"Last-Translator: \"Thi Huong Nguyen (thng)\" <thng@odoo.com>\n"
|
||||
"Language-Team: Vietnamese <https://translate.odoo.com/projects/odoo-19/"
|
||||
"hr_holidays/vi/>\n"
|
||||
|
|
@ -25,7 +25,7 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.14.3\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#. module: hr_holidays
|
||||
#. odoo-python
|
||||
|
|
@ -1607,7 +1607,7 @@ msgstr "Hàng ngày"
|
|||
#: model:ir.actions.act_window,name:hr_holidays.hr_leave_action_new_request
|
||||
#: model:ir.ui.menu,name:hr_holidays.hr_leave_menu_new_request
|
||||
msgid "Dashboard"
|
||||
msgstr "Bảng điều khiển"
|
||||
msgstr "Báo cáo tổng quan"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave__dashboard_warning_message
|
||||
|
|
@ -4301,7 +4301,7 @@ msgstr "Số đơn nghỉ phép"
|
|||
#. odoo-python
|
||||
#: code:addons/hr_holidays/models/hr_employee.py:0
|
||||
msgid "Time Off Dashboard"
|
||||
msgstr "Bảng điều khiển Ngày nghỉ"
|
||||
msgstr "Báo cáo tổng quan Ngày nghỉ"
|
||||
|
||||
#. module: hr_holidays
|
||||
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave__private_name
|
||||
|
|
|
|||
|
|
@ -139,22 +139,23 @@ class HrEmployee(models.Model):
|
|||
('employee_id', 'in', self.ids),
|
||||
('date_from', '<=', fields.Datetime.now()),
|
||||
('date_to', '>=', fields.Datetime.now()),
|
||||
('holiday_status_id.time_type', '=', 'leave'),
|
||||
('state', '=', 'validate'),
|
||||
])
|
||||
leave_data = {}
|
||||
for holiday in holidays:
|
||||
leave_data[holiday.employee_id.id] = {}
|
||||
leave_data.setdefault(holiday.employee_id.id, {})
|
||||
leave_data[holiday.employee_id.id]['leave_date_from'] = holiday.date_from.date()
|
||||
back_on = holiday.employee_id._get_first_working_interval(holiday.date_to)
|
||||
leave_data[holiday.employee_id.id]['leave_date_to'] = back_on.date() if back_on else None
|
||||
leave_data[holiday.employee_id.id]['current_leave_state'] = holiday.state
|
||||
leave_data[holiday.employee_id.id]['is_absent'] = leave_data[holiday.employee_id.id].get('is_absent') or holiday.holiday_status_id.time_type == 'leave'
|
||||
|
||||
for employee in self:
|
||||
employee.leave_date_from = leave_data.get(employee.id, {}).get('leave_date_from')
|
||||
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
|
||||
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
|
||||
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id).get('current_leave_state') == 'validate'
|
||||
employee_leave_data = leave_data.get(employee.id, {})
|
||||
employee.leave_date_from = employee_leave_data.get('leave_date_from')
|
||||
employee.leave_date_to = employee_leave_data.get('leave_date_to')
|
||||
employee.current_leave_state = employee_leave_data.get('current_leave_state')
|
||||
employee.is_absent = employee_leave_data and employee_leave_data.get('is_absent') and employee_leave_data.get('current_leave_state') == 'validate'
|
||||
|
||||
@api.depends('parent_id')
|
||||
def _compute_leave_manager(self):
|
||||
|
|
@ -411,6 +412,11 @@ class HrEmployee(models.Model):
|
|||
return self.env.user.employee_id
|
||||
|
||||
def _get_consumed_leaves(self, leave_types, target_date=False, ignore_future=False):
|
||||
""" This method won't call `_get_future_leaves_on` for the allocations contained by this variable (it will only use the current value of
|
||||
the `number_of_days` of the allocation, alias `number_of_hours_display`)
|
||||
|
||||
`precomputed_allocations`: context variable (recordset) which can be used to pass allocation that are considered to be already computed
|
||||
"""
|
||||
employees = self or self._get_contextual_employee()
|
||||
leaves_domain = [
|
||||
('holiday_status_id', 'in', leave_types.ids),
|
||||
|
|
@ -486,10 +492,16 @@ class HrEmployee(models.Model):
|
|||
'to_recheck_leaves': self.env['hr.leave']
|
||||
})
|
||||
)
|
||||
precomputed_allocations = self.env.context.get('precomputed_allocations')
|
||||
for allocation in allocations:
|
||||
allocation_data = allocations_leaves_consumed[allocation.employee_id][allocation.holiday_status_id][allocation]
|
||||
precomputed = False
|
||||
if precomputed_allocations:
|
||||
if allocation.id in precomputed_allocations.ids:
|
||||
allocation = precomputed_allocations.filtered(lambda alloc: alloc._origin.id == allocation.id)[0]
|
||||
precomputed = True
|
||||
future_leaves = 0
|
||||
if allocation.allocation_type == 'accrual':
|
||||
if allocation.allocation_type == 'accrual' and not precomputed:
|
||||
future_leaves = allocation._get_future_leaves_on(target_date)
|
||||
max_leaves = allocation.number_of_hours_display\
|
||||
if allocation.holiday_status_id.request_unit in ['hour']\
|
||||
|
|
@ -513,7 +525,11 @@ class HrEmployee(models.Model):
|
|||
allocations_with_date_to |= leave_allocation
|
||||
else:
|
||||
allocations_without_date_to |= leave_allocation
|
||||
sorted_leave_allocations = allocations_with_date_to.sorted(key='date_to') + allocations_without_date_to
|
||||
# Defines the order in which allocation will be used to take the leaves in priority
|
||||
sorted_leave_allocations = (
|
||||
allocations_with_date_to.sorted(key='date_to') +
|
||||
allocations_without_date_to.filtered(lambda alloc: alloc.allocation_type == 'accrual') +
|
||||
allocations_without_date_to.filtered(lambda alloc: alloc.allocation_type == 'regular'))
|
||||
|
||||
if leave_type.request_unit in ['day', 'half_day']:
|
||||
leave_duration_field = 'number_of_days'
|
||||
|
|
|
|||
|
|
@ -910,9 +910,11 @@ Versions:
|
|||
if any(leave.state == 'cancel' for leave in self):
|
||||
raise UserError(_('Only a manager can modify a canceled leave.'))
|
||||
|
||||
# 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')
|
||||
# If a leave changes state from validated or if the dates of a validated leave change
|
||||
# unlink the corresponding resource calendar leave
|
||||
date_fields = {'date_from', 'date_to', 'request_date_from', 'request_date_to'}
|
||||
validated_leaves = self.filtered(lambda l: l.state == 'validate')
|
||||
if validated_leaves and (('state' in values and values['state'] != 'validate') or date_fields.intersection(values)):
|
||||
validated_leaves._remove_resource_leave()
|
||||
|
||||
employee_id = values.get('employee_id', False)
|
||||
|
|
|
|||
|
|
@ -203,8 +203,8 @@ class HrLeaveAllocation(models.Model):
|
|||
|
||||
@api.depends('employee_id', 'holiday_status_id')
|
||||
def _compute_leaves(self):
|
||||
date_from = fields.Date.from_string(self.env.context['default_date_from']) if 'default_date_from' in self.env.context else fields.Date.today()
|
||||
employee_days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id, date_from)[0]
|
||||
date_from = fields.Date.today()
|
||||
employee_days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id, date_from, ignore_future=True)[0]
|
||||
for allocation in self:
|
||||
origin = allocation._origin
|
||||
virtual_leave = employee_days_per_allocation[origin.employee_id][origin.holiday_status_id][origin]
|
||||
|
|
@ -436,6 +436,16 @@ class HrLeaveAllocation(models.Model):
|
|||
The goal of this method is to retroactively apply accrual plan levels and progress from nextcall to date_to or today.
|
||||
If force_period is set, the accrual will run until date_to in a prorated way (used for end of year accrual actions).
|
||||
"""
|
||||
def _get_leaves_taken(allocation):
|
||||
precomputed_allocations = allocation
|
||||
if context_precomputed := self.env.context.get('precomputed_allocations'):
|
||||
precomputed_allocations |= context_precomputed
|
||||
# By setting `precomputed_allocations`, avoid infinite loop (otherwise _get_consumed_leaves -> _get_future_leaves_on -> _process_accrual_plans -> ...)
|
||||
employee_days_per_allocation = allocation.employee_id.with_context(precomputed_allocations=precomputed_allocations)._get_consumed_leaves(
|
||||
allocation.holiday_status_id, allocation.nextcall, ignore_future=True)[0]
|
||||
origin = allocation._origin
|
||||
leaves_taken = employee_days_per_allocation[origin.employee_id][origin.holiday_status_id][origin]['leaves_taken']
|
||||
return leaves_taken
|
||||
|
||||
date_to = date_to or fields.Date.today()
|
||||
already_accrued = {allocation.id: allocation.already_accrued or (allocation.number_of_days != 0 and allocation.accrual_plan_id.accrued_gain_time == 'start') for allocation in self}
|
||||
|
|
@ -452,10 +462,6 @@ class HrLeaveAllocation(models.Model):
|
|||
# even if the value doesn't change. This is the best performance atm.
|
||||
first_level = level_ids[0]
|
||||
first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
|
||||
if allocation.holiday_status_id.request_unit in ["day", "half_day"]:
|
||||
leaves_taken = allocation.leaves_taken
|
||||
else:
|
||||
leaves_taken = allocation.leaves_taken / allocation.employee_id._get_hours_per_day(allocation.date_from)
|
||||
allocation.already_accrued = already_accrued[allocation.id]
|
||||
# first time the plan is run, initialize nextcall and take carryover / level transition into account
|
||||
if not allocation.nextcall:
|
||||
|
|
@ -480,6 +486,10 @@ class HrLeaveAllocation(models.Model):
|
|||
# get current level and normal period boundaries, then set nextcall, adjusted for level transition and carryover
|
||||
# add days, trimmed if there is a maximum_leave
|
||||
while allocation.nextcall <= date_to:
|
||||
if allocation.holiday_status_id.request_unit in ["day", "half_day"]:
|
||||
leaves_taken = _get_leaves_taken(allocation)
|
||||
else:
|
||||
leaves_taken = _get_leaves_taken(allocation) / allocation.employee_id._get_hours_per_day(allocation.nextcall or allocation.date_from)
|
||||
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
|
||||
if not current_level:
|
||||
break
|
||||
|
|
@ -487,7 +497,7 @@ class HrLeaveAllocation(models.Model):
|
|||
if current_level.added_value_type == "day":
|
||||
current_level_maximum_leave = current_level.maximum_leave
|
||||
else:
|
||||
current_level_maximum_leave = current_level.maximum_leave / allocation.employee_id._get_hours_per_day(allocation.date_from)
|
||||
current_level_maximum_leave = current_level.maximum_leave / allocation.employee_id._get_hours_per_day(allocation.nextcall or allocation.date_from)
|
||||
nextcall = current_level._get_next_date(allocation.nextcall)
|
||||
# Since _get_previous_date returns the given date if it corresponds to a call date
|
||||
# this will always return lastcall except possibly on the first call
|
||||
|
|
@ -530,7 +540,7 @@ class HrLeaveAllocation(models.Model):
|
|||
# allocation.expiring_carryover_days - allocation.leaves_taken or 0 if all the expiring days were used
|
||||
# to take time off.
|
||||
# This ensures that only the days that weren't used to take time off will expire.
|
||||
expiring_days = max(0, allocation.expiring_carryover_days - allocation.leaves_taken)
|
||||
expiring_days = max(0, allocation.expiring_carryover_days - leaves_taken)
|
||||
allocation.number_of_days = max(0, allocation.number_of_days - expiring_days)
|
||||
allocation.expiring_carryover_days = 0
|
||||
|
||||
|
|
@ -571,6 +581,7 @@ class HrLeaveAllocation(models.Model):
|
|||
if allocation.accrual_plan_id.accrued_gain_time == 'start' and allocation.last_executed_carryover_date:
|
||||
last_carryover_date = allocation.last_executed_carryover_date
|
||||
carryover_level, carryover_level_idx = allocation._get_current_accrual_plan_level_id(last_carryover_date)
|
||||
carryover_period_start = carryover_level._get_previous_date(last_carryover_date)
|
||||
carryover_period_end = carryover_level._get_next_date(last_carryover_date)
|
||||
# Adjust carryover_period_end based on level_transition.
|
||||
if carryover_level_idx < (len(level_ids) - 1) and allocation.accrual_plan_id.transition_mode == 'immediately':
|
||||
|
|
@ -586,7 +597,8 @@ class HrLeaveAllocation(models.Model):
|
|||
# That is why (allocation.nextcall == period_end) is used instead of (is_accrual_date)
|
||||
accrued = not allocation.already_accrued and allocation.nextcall == period_end
|
||||
# If the days were accrued on the carryover period, then apply the carryover policy
|
||||
if accrued and last_carryover_date <= allocation.nextcall <= carryover_period_end:
|
||||
# If allocation.actual_lastcall == carryover_period_start, it means this loop has already been run once (skip to avoid applying the carryover twice)
|
||||
if accrued and last_carryover_date <= allocation.nextcall <= carryover_period_end and allocation.actual_lastcall != carryover_period_start:
|
||||
if carryover_level.action_with_unused_accruals == 'lost' or carryover_level.carryover_options == 'limited':
|
||||
allocation.last_executed_carryover_date = carryover_date
|
||||
allocated_days_left = allocation.number_of_days - leaves_taken
|
||||
|
|
@ -622,7 +634,8 @@ class HrLeaveAllocation(models.Model):
|
|||
current_level_maximum_leave = current_level.maximum_leave / allocation.employee_id._get_hours_per_day(allocation.date_from)
|
||||
if allocation.actual_lastcall in {period_start, allocation.date_from} | set(level_start.keys())\
|
||||
or (allocation.actual_lastcall - get_timedelta(current_level.accrual_validity_count, current_level.accrual_validity_type)
|
||||
in {period_start, allocation.date_from} | set(level_start.keys())):
|
||||
in {period_start, allocation.date_from} | set(level_start.keys())):
|
||||
leaves_taken = _get_leaves_taken(allocation)
|
||||
allocation._add_days_to_allocation(current_level, current_level_maximum_leave, leaves_taken, period_start, allocation.nextcall)
|
||||
allocation.already_accrued = True
|
||||
|
||||
|
|
@ -655,8 +668,8 @@ class HrLeaveAllocation(models.Model):
|
|||
and (not self.nextcall or self.nextcall <= accrual_date)):
|
||||
return 0
|
||||
|
||||
fake_allocation = self.env['hr.leave.allocation'].with_context(default_date_from=accrual_date).new(origin=self)
|
||||
fake_allocation.sudo().with_context(default_date_from=accrual_date)._process_accrual_plans(accrual_date, log=False)
|
||||
fake_allocation = self.env['hr.leave.allocation'].new(origin=self)
|
||||
fake_allocation.sudo()._process_accrual_plans(accrual_date, log=False)
|
||||
if self.holiday_status_id.request_unit in ['hour']:
|
||||
res = float_round(fake_allocation.number_of_hours_display - self.number_of_hours_display, precision_digits=2)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -675,8 +675,8 @@ class HrLeaveType(models.Model):
|
|||
def _get_carried_over_days_expiration_data(self, allocations, target_date):
|
||||
fake_allocations = self.env['hr.leave.allocation']
|
||||
for allocation in allocations:
|
||||
fake_allocations |= self.env['hr.leave.allocation'].with_context(default_date_from=target_date).new(origin=allocation)
|
||||
fake_allocations.sudo().with_context(default_date_from=target_date)._process_accrual_plans(target_date, log=False)
|
||||
fake_allocations |= self.env['hr.leave.allocation'].new(origin=allocation)
|
||||
fake_allocations.sudo()._process_accrual_plans(target_date, log=False)
|
||||
carried_over_days_expiration_data = {
|
||||
fake_allocation._origin:
|
||||
{
|
||||
|
|
|
|||
|
|
@ -95,7 +95,11 @@ class HrLeaveEmployeeTypeReport(models.Model):
|
|||
ON vl.employee_id = oa.employee_id
|
||||
AND vl.leave_type = oa.leave_type
|
||||
AND vl.date_from <= COALESCE(oa.date_to, 'infinity')
|
||||
AND vl.date_to >= oa.date_from
|
||||
AND (
|
||||
oa.date_to IS NULL
|
||||
OR
|
||||
vl.date_to >= oa.date_from
|
||||
)
|
||||
GROUP BY oa.allocation_id
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
import { Cache } from "@web/core/utils/cache";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
export class TimeOffCalendarModel extends CalendarModel {
|
||||
setup(params, services) {
|
||||
|
|
@ -35,11 +34,6 @@ export class TimeOffCalendarModel extends CalendarModel {
|
|||
result.title = [employee, result.title].join(" ");
|
||||
}
|
||||
}
|
||||
if (rawRecord.date_from && rawRecord.date_to) {
|
||||
const dateFrom = DateTime.fromSQL(rawRecord.date_from);
|
||||
const dateTo = DateTime.fromSQL(rawRecord.date_to);
|
||||
result.sameDay = dateFrom.hasSame(dateTo, 'day');
|
||||
}
|
||||
if (rawRecord.request_unit_half) {
|
||||
result.requestDateFromPeriod = rawRecord.request_date_from_period;
|
||||
result.requestDateToPeriod = rawRecord.request_date_to_period;
|
||||
|
|
|
|||
|
|
@ -29,14 +29,6 @@
|
|||
.hr_mandatory_day_#{$size - 1} {
|
||||
--mandatory-day-color: #{nth($o-colors, $size)};
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_half_left {
|
||||
clip-path: polygon(0 0, 50% 0, 50% 100%, 0% 100%);
|
||||
}
|
||||
|
||||
.o_event_half_right {
|
||||
clip-path: polygon(100% 0, 50% 0, 50% 100%, 100% 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,27 +67,83 @@ export class TimeOffCalendarYearRenderer extends CalendarYearRenderer {
|
|||
return [...super.getDayCellClassNames(info), ...this.mandatoryDays(info)];
|
||||
}
|
||||
|
||||
_halfDayStyleCache = new Set();
|
||||
ensureHalfDayClass(start, end) {
|
||||
const className = `o_event_half_${start}_${end}`;
|
||||
if (this._halfDayStyleCache.has(className)) return className;
|
||||
|
||||
const css = `
|
||||
.fc-event-start.${className} {
|
||||
clip-path: polygon(${start}% 0%, 100% 0%, 100% 100%, ${start}% 100%);
|
||||
}
|
||||
.fc-event-end.${className} {
|
||||
clip-path: polygon(0% 0%, ${end}% 0%, ${end}% 100%, 0% 100%);
|
||||
}
|
||||
.fc-event-start.fc-event-end.${className} {
|
||||
clip-path: polygon(${start}% 0%, ${end}% 0%, ${end}% 100%, ${start}% 100%);
|
||||
}
|
||||
`;
|
||||
let styleSheet = document.getElementById('half-day-dynamic-styles');
|
||||
if (!styleSheet) {
|
||||
styleSheet = document.createElement('style');
|
||||
styleSheet.id = 'half-day-dynamic-styles';
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
styleSheet.textContent += css;
|
||||
this._halfDayStyleCache.add(className);
|
||||
|
||||
return className;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
eventClassNames({ event }) {
|
||||
const classesToAdd = super.eventClassNames(...arguments);
|
||||
const record = this.props.model.records[event.id];
|
||||
if (record && record.requestDateFromPeriod && record.sameDay) {
|
||||
if (record.requestDateFromPeriod === "am" && record.requestDateToPeriod === "am") {
|
||||
classesToAdd.push("o_event_half_left")
|
||||
} else if (record.requestDateFromPeriod === "pm" && record.requestDateToPeriod === "pm") {
|
||||
classesToAdd.push("o_event_half_right")
|
||||
const classesToAdd = super.eventClassNames(...arguments);
|
||||
const record = this.props.model.records[event.id];
|
||||
if (record) {
|
||||
const isHalfStart = record.requestDateFromPeriod === "pm" ||
|
||||
(record?.rawRecord?.request_unit_hours && record.start.c.hour >= 12);
|
||||
const isHalfEnd = record.requestDateToPeriod === "am" ||
|
||||
(record?.rawRecord?.request_unit_hours && record.end.c.hour <= 12);
|
||||
|
||||
if (!isHalfStart && !isHalfEnd) return classesToAdd;
|
||||
|
||||
const isMultiWeek = record.start.localWeekNumber != record.end.localWeekNumber
|
||||
let start = 0;
|
||||
let end = 100;
|
||||
|
||||
if (!isMultiWeek) {
|
||||
const lastRowStart = record.start > record.end.startOf('month') ? record.start : record.end.startOf('month');
|
||||
const firstRowEnd = record.end < record.start.endOf('month') ? record.end : record.start.endOf('month');
|
||||
const daysInFirstRow = firstRowEnd.startOf('day').diff(record.start.startOf('day'), 'days').days + 1;
|
||||
const daysInLastRow = record.end.startOf('day').diff(lastRowStart.startOf('day'), 'days').days + 1;
|
||||
|
||||
start = isHalfStart ? Math.round(50 / daysInFirstRow) : 0;
|
||||
end = isHalfEnd ? Math.round(100 - (50 / daysInLastRow)) : 100;
|
||||
}
|
||||
else {
|
||||
// Multi-week: first slice — only care about start
|
||||
if (isHalfStart) {
|
||||
const rowEnd = record.start.endOf('week') < record.start.endOf('month')
|
||||
? record.start.endOf('week').minus({ days: 1 })
|
||||
: record.start.endOf('month');
|
||||
const daysInFirstRow = rowEnd.startOf('day').diff(record.start.startOf('day'), 'days').days + 1;
|
||||
start = Math.round(50 / daysInFirstRow);
|
||||
}
|
||||
// Multi-week: last slice — only care about end
|
||||
if (isHalfEnd) {
|
||||
const rowStart = record.end.startOf('week') > record.end.startOf('month')
|
||||
? record.end.startOf('week').minus({ days: 1 })
|
||||
: record.end.startOf('month');
|
||||
const daysInLastRow = record.end.startOf('day').diff(rowStart.startOf('day'), 'days').days + 1;
|
||||
end = Math.round(100 - (50 / daysInLastRow));
|
||||
}
|
||||
}
|
||||
// handling half pill UX for custom_hours
|
||||
if (record?.rawRecord?.request_unit_hours && record.sameDay) {
|
||||
if (record.end.c.hour < 12) {
|
||||
classesToAdd.push("o_event_half_left");
|
||||
} else if (record.end.c.hour >= 12 && record.start.c.hour >= 12) {
|
||||
classesToAdd.push("o_event_half_right");
|
||||
}
|
||||
}
|
||||
return classesToAdd;
|
||||
|
||||
classesToAdd.push(this.ensureHalfDayClass(start, end));
|
||||
}
|
||||
return classesToAdd;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,3 +35,4 @@ from . import test_leave_type_data
|
|||
from . import test_multi_contract
|
||||
from . import test_time_off_allocation_tour
|
||||
from . import test_flexible_resource_calendar
|
||||
from . import test_hr_leave_report
|
||||
|
|
|
|||
|
|
@ -112,6 +112,16 @@ class TestHrHolidaysCommon(common.TransactionCase):
|
|||
self.assertEqual(allocation_data[employee][0][1]['remaining_leaves'],
|
||||
value, f"Remaining leaves for date '{date}' are incorrect.")
|
||||
|
||||
def _take_leave(self, employee, leave_type, date_from, date_to):
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': 'Leave',
|
||||
'employee_id': employee.id,
|
||||
'holiday_status_id': leave_type.id,
|
||||
'request_date_from': date_from,
|
||||
'request_date_to': date_to,
|
||||
})
|
||||
return leave
|
||||
|
||||
def _create_form_test_accrual_allocation(self, leave_type, date_from, employee, accrual_plan, date_to=None, creator_user=None):
|
||||
allocation = self.env['hr.leave.allocation']
|
||||
if creator_user:
|
||||
|
|
@ -127,6 +137,21 @@ class TestHrHolidaysCommon(common.TransactionCase):
|
|||
form.date_to = date_to
|
||||
return form.record
|
||||
|
||||
def _create_form_test_regular_allocation(self, leave_type, date_from, employee, number_of_days, date_to=None, creator_user=None):
|
||||
allocation = self.env['hr.leave.allocation']
|
||||
if creator_user:
|
||||
allocation = allocation.with_user(creator_user)
|
||||
with Form(allocation, 'hr_holidays.hr_leave_allocation_view_form_manager') as form:
|
||||
form.name = 'Test regular allocation'
|
||||
form.allocation_type = 'regular'
|
||||
form.employee_id = employee
|
||||
form.holiday_status_id = leave_type
|
||||
form.date_from = date_from
|
||||
form.number_of_days_display = number_of_days
|
||||
if date_to:
|
||||
form.date_to = date_to
|
||||
return form.record
|
||||
|
||||
|
||||
class TestHolidayContract(TransactionCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,56 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
|
|||
'allocation_validation_type': 'no_validation',
|
||||
'request_unit': 'day',
|
||||
})
|
||||
cls.accrual_plan_monthly_end = cls.env['hr.leave.accrual.plan'].create({
|
||||
'name': 'Accrual Plan For Test',
|
||||
'is_based_on_worked_time': False,
|
||||
'accrued_gain_time': 'end',
|
||||
'carryover_date': 'allocation',
|
||||
'can_be_carryover': True,
|
||||
'level_ids': [Command.create({
|
||||
'start_count': 0,
|
||||
'added_value_type': 'day',
|
||||
'added_value': 2,
|
||||
'frequency': 'monthly',
|
||||
'action_with_unused_accruals': 'all',
|
||||
'cap_accrued_time': False,
|
||||
})],
|
||||
})
|
||||
cls.accrual_plan_monthly_end_max_leaves = cls.env['hr.leave.accrual.plan'].create({
|
||||
'name': 'Accrual Plan For Test',
|
||||
'is_based_on_worked_time': False,
|
||||
'accrued_gain_time': 'end',
|
||||
'carryover_date': 'allocation',
|
||||
'can_be_carryover': True,
|
||||
'level_ids': [Command.create({
|
||||
'start_count': 0,
|
||||
'added_value_type': 'day',
|
||||
'added_value': 2,
|
||||
'frequency': 'monthly',
|
||||
'action_with_unused_accruals': 'all',
|
||||
'cap_accrued_time': True,
|
||||
'maximum_leave': 10,
|
||||
})],
|
||||
})
|
||||
cls.accrual_plan_yearly_max_postponed_days_start = cls.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
|
||||
'name': '21 days per year, 5 carryover max',
|
||||
'transition_mode': 'immediately',
|
||||
'carryover_date': 'year_start',
|
||||
'accrued_gain_time': 'start',
|
||||
'can_be_carryover': True,
|
||||
'level_ids': [
|
||||
Command.create({
|
||||
"start_count": 0,
|
||||
"added_value": 21,
|
||||
"frequency": "yearly",
|
||||
"yearly_day": 1,
|
||||
"yearly_month": "1",
|
||||
"action_with_unused_accruals": "all",
|
||||
"carryover_options": "limited",
|
||||
"postpone_max_days": 5,
|
||||
})
|
||||
],
|
||||
})
|
||||
|
||||
def setAllocationCreateDate(self, allocation_id, date):
|
||||
""" This method is a hack in order to be able to define/redefine the create_date
|
||||
|
|
@ -4500,6 +4550,253 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
|
|||
self.assert_remaining_leaves_equal(self.leave_type_day, remaining_leaves, self.employee_emp, test_date, digits=3)
|
||||
self.assertAlmostEqual(allocation.expiring_carryover_days, expiring_days, 2, msg=f'Incorrect number of expiring days for {test_date}')
|
||||
|
||||
def test_accrual_allocation_immediate_monthly_start_day(self):
|
||||
""" Test fix for incorrect accrued days when changing date_from on accrual allocations. """
|
||||
with freeze_time('2024-11-15'):
|
||||
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
|
||||
'name': 'Accrual Plan For Test',
|
||||
'accrued_gain_time': 'start',
|
||||
'carryover_date': 'other',
|
||||
'carryover_day': 1,
|
||||
'carryover_month': '1',
|
||||
'level_ids': [(0, 0, {
|
||||
'added_value': 2,
|
||||
'added_value_type': 'day',
|
||||
'frequency': 'monthly',
|
||||
'cap_accrued_time': True,
|
||||
'maximum_leave': 10000,
|
||||
'start_count': 0,
|
||||
'start_type': 'day',
|
||||
'action_with_unused_accruals': 'all',
|
||||
})],
|
||||
})
|
||||
|
||||
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
|
||||
'name': 'Accrual allocation for employee',
|
||||
'accrual_plan_id': accrual_plan.id,
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'number_of_days': 0,
|
||||
'allocation_type': 'accrual',
|
||||
'date_from': datetime.date(2024, 11, 1),
|
||||
})
|
||||
allocation._onchange_date_from()
|
||||
self.assertAlmostEqual(allocation.number_of_days_display, 2, places=2, msg="Accrued days should be 2 (2 days for Nov).")
|
||||
|
||||
allocation.date_from = datetime.date(2024, 10, 1)
|
||||
allocation._onchange_date_from()
|
||||
allocation._update_accrual()
|
||||
|
||||
self.assertAlmostEqual(allocation.number_of_days_display, 4, places=2, msg="Accrued days should be 4 (4 days for Nov).")
|
||||
|
||||
def test_modify_cap_accrued_days(self):
|
||||
"""
|
||||
Context: allocation with a 1 level accrual plan which
|
||||
- Carry all days over
|
||||
- Accrues monthly at the end of the period
|
||||
Assert the virtual remaining leaves of the employee drop to `maximum_leave` when setting `cap_accrued_time` of the accrual plan
|
||||
to `True` and the virtual remaining leaves was bigger than `maximum_leave`
|
||||
"""
|
||||
with freeze_time('2020-01-01'):
|
||||
accrued_days = 2
|
||||
accrual_plan = self.accrual_plan_monthly_end
|
||||
leave_type_day = self.leave_type_day
|
||||
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2020-01-01', self.employee_emp, accrual_plan)
|
||||
allocation.action_approve()
|
||||
|
||||
with freeze_time('2022-01-01'):
|
||||
allocation._update_accrual()
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 24 * accrued_days, self.employee_emp)
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 25 * accrued_days, self.employee_emp, date='2022-02-01')
|
||||
|
||||
accrual_plan.level_ids.update({'maximum_leave': 21, 'cap_accrued_time': True})
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 21, self.employee_emp, date='2022-02-01')
|
||||
|
||||
with freeze_time('2022-02-01'):
|
||||
allocation._update_accrual()
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 21, self.employee_emp)
|
||||
|
||||
def test_modify_cap_accrued_days_with_leaves(self):
|
||||
"""
|
||||
Context: allocation with a 1 level accrual plan which
|
||||
- Carry all days over
|
||||
- Accrues monthly at the end of the period
|
||||
Assert the virtual remaining leaves of the employee drop to `maximum_leave` when setting `cap_accrued_time` of the accrual plan
|
||||
to `True` and the virtual remaining leaves was bigger than `maximum_leave`
|
||||
Adds leaves in the computation (only difference with `test_modify_cap_accrued_days`)
|
||||
"""
|
||||
with freeze_time('2020-01-01'):
|
||||
accrued_days = 2
|
||||
accrual_plan = self.accrual_plan_monthly_end
|
||||
leave_type_day = self.leave_type_day
|
||||
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2020-01-01', self.employee_emp, accrual_plan)
|
||||
allocation.action_approve()
|
||||
|
||||
with freeze_time('2022-01-01'):
|
||||
allocation._update_accrual()
|
||||
self.assert_remaining_leaves_equal(leave_type_day, before_leave_days := 24 * accrued_days, self.employee_emp)
|
||||
# 35 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-02-18')._action_validate()
|
||||
# 10 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2022-03-07', '2022-03-18')._action_validate()
|
||||
|
||||
with freeze_time('2022-03-01'):
|
||||
allocation._update_accrual()
|
||||
# before_leave_days - 35 days (first leave) + 2 months accrual
|
||||
self.assert_remaining_leaves_equal(leave_type_day, after_leave := before_leave_days - 35 + 2 * accrued_days, self.employee_emp)
|
||||
accrual_plan.level_ids.update({'maximum_leave': 21, 'cap_accrued_time': True})
|
||||
self.assert_remaining_leaves_equal(leave_type_day, min(after_leave, 21), self.employee_emp)
|
||||
|
||||
with freeze_time('2022-04-01'):
|
||||
allocation._update_accrual()
|
||||
after_leave2 = min(after_leave, 21) - 10
|
||||
self.assert_remaining_leaves_equal(leave_type_day, min(after_leave2 + accrued_days, 21), self.employee_emp)
|
||||
self.assert_remaining_leaves_equal(leave_type_day, min(after_leave2 + 12 * accrued_days, 21), self.employee_emp, date='2023-03-01')
|
||||
|
||||
def test_get_allocation_actual_future_leaves(self):
|
||||
"""
|
||||
Context: allocation with a 1 level accrual plan which
|
||||
- Carry all days over
|
||||
- Accrues monthly at the end of the period
|
||||
- Has maximum 10 leaves
|
||||
Assert the virtual remaining leaves of the employee allocation are not frozen while taking multiple leaves
|
||||
and using `get_allocation_data`.
|
||||
"""
|
||||
with freeze_time('2019-01-01'):
|
||||
accrual_plan = self.accrual_plan_monthly_end_max_leaves
|
||||
leave_type_day = self.leave_type_day
|
||||
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2019-01-01', self.employee_emp, accrual_plan)
|
||||
allocation.action_approve()
|
||||
|
||||
with freeze_time('2022-01-01'):
|
||||
allocation._update_accrual()
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp)
|
||||
|
||||
# 10 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-01-14')._action_validate()
|
||||
# 10 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2023-01-02', '2023-01-13')._action_validate()
|
||||
# 10 days leaves that shouldn't be taken into account in this test
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2025-10-06', '2025-10-17')._action_validate()
|
||||
|
||||
with freeze_time('2023-01-01'):
|
||||
allocation._update_accrual()
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp)
|
||||
|
||||
with freeze_time('2023-02-01'):
|
||||
allocation._update_accrual()
|
||||
# 10 days - 10 days (leave) + 2 days (1 month accrual)
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 2, self.employee_emp)
|
||||
|
||||
def test_get_allocation_future_leaves(self):
|
||||
"""
|
||||
Context: allocation with a 1 level accrual plan which
|
||||
- Carry all days over
|
||||
- Accrues monthly at the end of the period
|
||||
- Has maximum 10 leaves
|
||||
Assert the virtual remaining leaves of the employee allocation are not frozen while taking multiple leaves
|
||||
and using `get_allocation_data` with the `target_date` parameter set in the future.
|
||||
"""
|
||||
with freeze_time('2019-01-01'):
|
||||
accrual_plan = self.accrual_plan_monthly_end_max_leaves
|
||||
leave_type_day = self.leave_type_day
|
||||
allocation = self._create_form_test_accrual_allocation(leave_type_day, '2019-01-01', self.employee_emp, accrual_plan)
|
||||
allocation.action_approve()
|
||||
|
||||
with freeze_time('2022-01-01'):
|
||||
allocation._update_accrual()
|
||||
# Max number of leaves for the only level of the accrual plan is 10
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp)
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 10, self.employee_emp, date='2022-02-01')
|
||||
|
||||
# 10 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-01-14')._action_validate()
|
||||
# 10 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2023-01-02', '2023-01-13')._action_validate()
|
||||
|
||||
# 10 days leaves that shouldn't be taken into account in this test
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2025-10-06', '2025-10-17')._action_validate()
|
||||
# Right after spending all the 10 leaves ('2023-01-02' -> '2023-01-13')
|
||||
# 10 days - 10 days (leave) + 2 days (1 month accrual)
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 2, self.employee_emp, date='2023-02-01')
|
||||
|
||||
def _test_get_allocation_future_leaves_regular(self, regular_before):
|
||||
"""
|
||||
Context:
|
||||
1) Allocation with a 1 level accrual plan which
|
||||
- Carry all days over
|
||||
- Accrues monthly at the end of the period
|
||||
- Has maximum 10 leaves
|
||||
2) Regular allocation
|
||||
Assert the virtual remaining leaves of the employee allocation are not frozen while taking multiple leaves
|
||||
and using `get_allocation_data` with the `target_date` parameter set in the future.
|
||||
:param regular_before: set the `date_from` of the regular allocation before the `date_from` of the accrual allocation
|
||||
"""
|
||||
leave_type_day = self.leave_type_day
|
||||
if regular_before:
|
||||
with freeze_time('2018-01-01'):
|
||||
self._create_form_test_regular_allocation(leave_type_day, '2018-01-01', self.employee_emp, number_of_days=10)
|
||||
|
||||
with freeze_time('2019-01-01'):
|
||||
accrual_plan = self.accrual_plan_monthly_end_max_leaves
|
||||
accrual_allocation = self._create_form_test_accrual_allocation(leave_type_day, '2019-01-01', self.employee_emp, accrual_plan)
|
||||
accrual_allocation.action_approve()
|
||||
|
||||
if not regular_before:
|
||||
with freeze_time('2020-01-01'):
|
||||
self._create_form_test_regular_allocation(leave_type_day, '2020-01-01', self.employee_emp, number_of_days=10)
|
||||
|
||||
with freeze_time('2022-01-01'):
|
||||
accrual_allocation._update_accrual()
|
||||
# Max number of leaves for the only level of the accrual plan is 10 + 10 for the regular allocation
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 20, self.employee_emp)
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 20, self.employee_emp, date='2022-02-01')
|
||||
|
||||
# 10 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2022-01-03', '2022-01-14')._action_validate()
|
||||
# 10 days leave
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2023-01-02', '2023-01-13')._action_validate()
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 12, self.employee_emp, date='2023-02-01')
|
||||
|
||||
# 10 days leaves that shouldn't be taken into account in this test
|
||||
self._take_leave(self.employee_emp, leave_type_day, '2023-10-06', '2023-10-17')._action_validate()
|
||||
# Right after spending all the 10 leaves ('2023-01-02' -> '2023-01-13')
|
||||
# 10 days - 10 days (leave) + 2 days (1 month accrual)
|
||||
self.assert_remaining_leaves_equal(leave_type_day, 12, self.employee_emp, date='2023-02-01')
|
||||
|
||||
def test_get_allocation_future_leaves_regular1(self):
|
||||
self._test_get_allocation_future_leaves_regular(regular_before=False)
|
||||
|
||||
def test_get_allocation_future_leaves_regular2(self):
|
||||
self._test_get_allocation_future_leaves_regular(regular_before=True)
|
||||
|
||||
def test_accrual_days_left_over_carryover_maximum_with_leaves_around_carryover(self):
|
||||
with freeze_time('2024-11-25'):
|
||||
allocation = self._create_form_test_accrual_allocation(
|
||||
self.leave_type, '2024-01-01', self.employee_emp, self.accrual_plan_yearly_max_postponed_days_start)
|
||||
allocation.action_approve()
|
||||
|
||||
# take 10 days in the past
|
||||
leave = self._take_leave(self.employee_emp, self.leave_type, '2024-12-09', '2024-12-20')
|
||||
leave._action_validate()
|
||||
# take 10 days in January
|
||||
leave_2 = self._take_leave(self.employee_emp, self.leave_type, '2025-01-06', '2025-01-17')
|
||||
leave_2._action_validate()
|
||||
|
||||
# The remaining leaves on a specific date should be:
|
||||
# 25/11/2024 to 08/12/2024: 21 days, no leave are deducted
|
||||
# 09/12/2024 to 31/12/2024: 11 days, the first leave is deducted as its start date is past
|
||||
# 01/01/2025 to 05/01/2025: 26 days, carryover occured, from the 11 days only 5 are left, then the yearly 21 days are added
|
||||
# from 06/01/2025: 16 days, the second leave is deducted as its start date is past
|
||||
assertions = [
|
||||
('2024-12-01', 21.0),
|
||||
('2024-12-15', 11.0),
|
||||
('2025-01-02', 26.0),
|
||||
('2025-01-06', 16.0),
|
||||
]
|
||||
for test_date, expected_remaining_leaves in assertions:
|
||||
self.assert_remaining_leaves_equal(self.leave_type, expected_remaining_leaves, self.employee_emp, test_date, 2)
|
||||
|
||||
def test_accrual_leaves_cancel_cron_with_refused_allocation(self):
|
||||
""" Test that the _cancel_invalid_leaves cron cancels leaves without valid allocation"""
|
||||
leave_type = self.env['hr.leave.type'].create({
|
||||
|
|
@ -4537,3 +4834,50 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
|
|||
allocation.action_refuse()
|
||||
self.env['hr.leave']._cancel_invalid_leaves()
|
||||
self.assertEqual(leave.state, 'cancel')
|
||||
|
||||
def test_timeoff_allocation_with_unused_accrual_lost(self):
|
||||
"""
|
||||
Create an accrual plan:
|
||||
* Set the accrued gain time to "At the start of the accrual period"
|
||||
* Set the carry-over time to "At the start of the year"
|
||||
Create a milestone:
|
||||
* Set the number of accrued days to 1
|
||||
* Set the accrual frequency to "monthly" and
|
||||
the carry over to "None.Accrued time reset to 0"
|
||||
Create an allocation:
|
||||
* Set the start date to 2025-01-01
|
||||
* Set the accrual plan to the one created above
|
||||
Use future allocations to see the number of days accrued on
|
||||
2026-02-01(feb). It should be 2.
|
||||
"""
|
||||
with freeze_time('2025-01-01'):
|
||||
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
|
||||
'name': 'Accrual Plan For Test',
|
||||
'accrued_gain_time': 'start',
|
||||
'level_ids': [Command.create({
|
||||
'start_count': 0,
|
||||
'frequency': 'monthly',
|
||||
'action_with_unused_accruals': 'lost',
|
||||
})],
|
||||
})
|
||||
allocation = self.env['hr.leave.allocation'].create({
|
||||
'name': 'Accrual allocation for employee',
|
||||
'accrual_plan_id': accrual_plan.id,
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': '2025-01-01',
|
||||
'allocation_type': 'accrual',
|
||||
'number_of_days': 0,
|
||||
'already_accrued': False,
|
||||
})
|
||||
allocation.action_approve()
|
||||
assertions = (
|
||||
# Do not run the update on 2026-01-01 otherwise the bug disappears on 2026-02-01
|
||||
# ('2026-01-01', 1),
|
||||
('2026-02-01', 2),
|
||||
('2026-03-01', 3),
|
||||
)
|
||||
for test_date, expected_remaining_leaves in assertions:
|
||||
with freeze_time(test_date):
|
||||
allocation._update_accrual()
|
||||
self.assertEqual(allocation.number_of_days, expected_remaining_leaves)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
|
||||
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
|
||||
|
||||
|
||||
class TestHrLeaveReport(TestHrHolidaysCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.overtime_leave_type = cls.env['hr.leave.type'].create({
|
||||
'name': 'Overtime Type',
|
||||
'requires_allocation': 'yes',
|
||||
'leave_validation_type': 'no_validation',
|
||||
'request_unit': 'hour',
|
||||
'time_type': 'leave',
|
||||
})
|
||||
|
||||
def test_hr_leave_employee_report(self):
|
||||
self.env['hr.leave.allocation'].create([
|
||||
{
|
||||
'name': 'Overtime',
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.overtime_leave_type.id,
|
||||
'date_from': '2025-12-01',
|
||||
'number_of_days': '1.875',
|
||||
},
|
||||
{
|
||||
'name': 'Overtime',
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.overtime_leave_type.id,
|
||||
'date_from': '2026-01-01',
|
||||
'number_of_days': '12.6875',
|
||||
},
|
||||
{
|
||||
'name': 'Overtime',
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.overtime_leave_type.id,
|
||||
'date_from': '2026-02-01',
|
||||
'number_of_days': '1.5',
|
||||
},
|
||||
]).action_approve()
|
||||
|
||||
self.env['hr.leave'].create([
|
||||
{
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.overtime_leave_type.id,
|
||||
'request_date_from': '2025-12-02',
|
||||
'request_date_to': '2025-12-02',
|
||||
'request_unit_hours': True,
|
||||
'request_hour_from': 8,
|
||||
'request_hour_to': 17,
|
||||
},
|
||||
{
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.overtime_leave_type.id,
|
||||
'request_date_from': '2026-01-02',
|
||||
'request_date_to': '2026-01-02',
|
||||
'request_unit_hours': True,
|
||||
'request_hour_from': 8,
|
||||
'request_hour_to': 17,
|
||||
},
|
||||
{
|
||||
'employee_id': self.employee_emp.id,
|
||||
'holiday_status_id': self.overtime_leave_type.id,
|
||||
'request_date_from': '2026-02-02',
|
||||
'request_date_to': '2026-02-02',
|
||||
'request_unit_hours': True,
|
||||
'request_hour_from': 8,
|
||||
'request_hour_to': 17,
|
||||
},
|
||||
]).action_approve()
|
||||
|
||||
self.env.flush_all()
|
||||
|
||||
domain = [
|
||||
('employee_id', '=', self.employee_emp.id),
|
||||
('leave_type', '=', self.overtime_leave_type.id),
|
||||
]
|
||||
leave_balance = self.env['hr.leave.employee.type.report'].search(domain)
|
||||
|
||||
left_allocation = leave_balance.filtered(lambda l: l.holiday_status == 'left')
|
||||
taken_allocation = leave_balance.filtered(lambda l: l.holiday_status == 'taken')
|
||||
|
||||
self.assertEqual(sum(left_allocation.mapped('number_of_hours')), 104.5)
|
||||
self.assertEqual(sum(taken_allocation.mapped('number_of_hours')), 24.0)
|
||||
|
|
@ -45,6 +45,8 @@ class TestHrLeaveType(TestHrHolidaysCommon):
|
|||
with freeze_time('2025-09-03 13:00:00'):
|
||||
employee._compute_leave_status()
|
||||
self.assertFalse(employee.is_absent)
|
||||
self.assertEqual(employee.leave_date_from, leave_0.request_date_from)
|
||||
self.assertEqual(employee.leave_date_to, leave_0.employee_id._get_first_working_interval(leave_0.date_to).date())
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
leave_1 = self.env['hr.leave'].create({
|
||||
|
|
|
|||
|
|
@ -2185,3 +2185,137 @@ class TestLeaveRequests(TestHrHolidaysCommon):
|
|||
})
|
||||
|
||||
self.assertEqual(leave.number_of_hours, 13.0)
|
||||
|
||||
def test_group_leave_conflicting_days_computation(self):
|
||||
"""Test that a group leave that overrides existing approved time off days
|
||||
correctly computes the duration of each leave.
|
||||
"""
|
||||
LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id)
|
||||
self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).create({
|
||||
'name': 'Annual Time Off',
|
||||
'employee_id': self.employee_emp_id,
|
||||
'holiday_status_id': self.holidays_type_4.id,
|
||||
'number_of_days': 20,
|
||||
'date_from': '2026-01-01',
|
||||
}).action_approve()
|
||||
|
||||
# Create existing approved time off: Feb 23 - Feb 24 (2 days) and Feb 26 - Feb 27
|
||||
leave1, leave2 = self.env['hr.leave'].with_user(self.user_employee_id).create([
|
||||
{
|
||||
'name': 'Approved Leave 1',
|
||||
'employee_id': self.employee_emp_id,
|
||||
'holiday_status_id': self.holidays_type_4.id,
|
||||
'request_date_from': '2026-02-23',
|
||||
'request_date_to': '2026-02-24',
|
||||
},
|
||||
{
|
||||
'name': 'Approved Leave 2',
|
||||
'employee_id': self.employee_emp_id,
|
||||
'holiday_status_id': self.holidays_type_1.id,
|
||||
'request_date_from': '2026-02-26',
|
||||
'request_date_to': '2026-02-27',
|
||||
}])
|
||||
|
||||
leave1.with_user(self.user_hrmanager_id).action_approve()
|
||||
self.assertEqual(leave1.number_of_days, 2, "Approved Leave 1 should be 2 days")
|
||||
|
||||
leave2.with_user(self.user_hrmanager_id).action_approve()
|
||||
self.assertEqual(leave2.number_of_days, 2, "Approved Leave 2 should be 2 days")
|
||||
|
||||
# Create Training Leave Type
|
||||
training_type = LeaveType.create({
|
||||
'name': 'Training',
|
||||
'requires_allocation': False,
|
||||
'leave_validation_type': 'no_validation',
|
||||
})
|
||||
|
||||
# Use the Wizard to create a Training for the whole company: Feb 24 - Feb 26
|
||||
# This overlaps with two days of approved allocated leaves
|
||||
# Last day of leave 1 and first day of leave 2
|
||||
leave_wizard_form = Form(self.env['hr.leave.generate.multi.wizard'].with_user(self.user_hrmanager_id))
|
||||
leave_wizard_form.allocation_mode = 'company'
|
||||
leave_wizard_form.company_id = self.env.company
|
||||
leave_wizard_form.holiday_status_id = training_type
|
||||
leave_wizard_form.date_from = date(2026, 2, 24)
|
||||
leave_wizard_form.date_to = date(2026, 2, 26)
|
||||
leave_wizard = leave_wizard_form.save()
|
||||
leave_wizard.action_generate_time_off()
|
||||
|
||||
generated_training = self.env['hr.leave'].search([
|
||||
('employee_id', '=', self.employee_emp_id),
|
||||
('holiday_status_id', '=', training_type.id),
|
||||
('request_date_from', '=', '2026-02-24')
|
||||
])
|
||||
|
||||
# ASSERTS
|
||||
# Assert correct duration calculation for the training leave
|
||||
self.assertEqual(generated_training.number_of_days, 3.0,
|
||||
"The training (Feb 25-27) should be 3 days, since it overrides other leaves.")
|
||||
|
||||
# Assert the original time off was split correctly
|
||||
# It should now only cover Feb 23 (1 day) and Feb 27 (1 day)
|
||||
leave1.invalidate_recordset(['number_of_days', 'request_date_to'])
|
||||
self.assertEqual(leave1.request_date_to, date(2026, 2, 23),
|
||||
"Leave 1 should have been shortened to end before the training.")
|
||||
self.assertEqual(leave1.number_of_days, 1.0,
|
||||
"Leave 1 duration should have been updated to 1 day.")
|
||||
|
||||
leave2.invalidate_recordset(['number_of_days', 'request_date_to'])
|
||||
self.assertEqual(leave2.request_date_from, date(2026, 2, 27),
|
||||
"Leave 2 should have been shortened to start after the training.")
|
||||
self.assertEqual(leave2.number_of_days, 1.0,
|
||||
"Leave 2 duration should have been updated to 1 day.")
|
||||
|
||||
def test_group_leave_hourly_conflict(self):
|
||||
"""Ensure batch generation fails if overlapping hourly time off exists
|
||||
and does not unlink the related calendar leaves."""
|
||||
|
||||
# Create an hourly leave and validate it
|
||||
LeaveType = self.env['hr.leave.type'].with_user(self.user_hrmanager_id)
|
||||
hourly_type = LeaveType.create({
|
||||
'name': 'Hourly Leave',
|
||||
'request_unit': 'hour',
|
||||
'requires_allocation': False,
|
||||
'leave_validation_type': 'both',
|
||||
})
|
||||
hourly_leave = self.env['hr.leave'].with_user(self.user_employee_id).create({
|
||||
'name': 'Hourly Leave',
|
||||
'employee_id': self.employee_emp_id,
|
||||
'holiday_status_id': hourly_type.id,
|
||||
'request_unit_hours': True,
|
||||
'request_date_from': '2026-02-24',
|
||||
'request_date_to': '2026-02-24',
|
||||
'request_hour_from': 8,
|
||||
'request_hour_to': 12,
|
||||
})
|
||||
hourly_leave.with_user(self.user_hrmanager_id).action_approve()
|
||||
|
||||
# Check that the leave exists and is linked to a calendar leave
|
||||
calendar_leave = self.env['resource.calendar.leaves'].search([
|
||||
('holiday_id', '=', hourly_leave.id)
|
||||
])
|
||||
self.assertTrue(calendar_leave)
|
||||
|
||||
# Create a group leave that overlaps with the hourly leave
|
||||
training_type = LeaveType.create({
|
||||
'name': 'Training',
|
||||
'requires_allocation': False,
|
||||
'leave_validation_type': 'no_validation',
|
||||
})
|
||||
leave_wizard_form = Form(self.env['hr.leave.generate.multi.wizard'].with_user(self.user_hrmanager_id))
|
||||
leave_wizard_form.allocation_mode = 'company'
|
||||
leave_wizard_form.company_id = self.env.company
|
||||
leave_wizard_form.holiday_status_id = training_type
|
||||
leave_wizard_form.date_from = date(2026, 2, 24)
|
||||
leave_wizard_form.date_to = date(2026, 2, 24)
|
||||
leave_wizard = leave_wizard_form.save()
|
||||
|
||||
# ASSERTIONS
|
||||
# Should raise an error and the approved leave should not be changed or removed
|
||||
with self.assertRaises(UserError):
|
||||
leave_wizard.action_generate_time_off()
|
||||
|
||||
self.assertTrue(calendar_leave.exists(), "Calendar leaves should not be unlinked on error")
|
||||
|
||||
hourly_leave.invalidate_recordset()
|
||||
self.assertEqual(hourly_leave.state, 'validate')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue