payroll management timeoff
This commit is contained in:
parent
bc2d3d1b15
commit
944d2e6227
|
|
@ -44,7 +44,14 @@ class HrPayslipWorkedDays(models.Model):
|
||||||
if worked_days.payslip_id.wage_type == "hourly":
|
if worked_days.payslip_id.wage_type == "hourly":
|
||||||
worked_days.amount = worked_days.payslip_id.contract_id.hourly_wage * worked_days.number_of_hours if worked_days.is_paid else 0
|
worked_days.amount = worked_days.payslip_id.contract_id.hourly_wage * worked_days.number_of_hours if worked_days.is_paid else 0
|
||||||
else:
|
else:
|
||||||
worked_days.amount = worked_days.payslip_id.contract_id.contract_wage * worked_days.number_of_hours / (worked_days.payslip_id.sum_worked_hours or 1) if worked_days.is_paid else 0
|
days_count = float((worked_days.payslip_id.date_to - worked_days.payslip_id.date_from).days + 1)
|
||||||
|
hours_per_day = worked_days.payslip_id._get_worked_day_lines_hours_per_day()
|
||||||
|
if worked_days.work_entry_type_id.is_leave:
|
||||||
|
daily_wage = float(worked_days.payslip_id.contract_id.contract_wage / days_count)
|
||||||
|
worked_days.amount = (daily_wage * round(worked_days.number_of_hours/hours_per_day, 5 if hours_per_day else 0)) if worked_days.is_paid else -(daily_wage * round(worked_days.number_of_hours/hours_per_day, 5))
|
||||||
|
else:
|
||||||
|
days_to_remove = round((worked_days.payslip_id.sum_worked_hours - worked_days.number_of_hours)/hours_per_day, 5) if hours_per_day else 0
|
||||||
|
worked_days.amount = worked_days.payslip_id.contract_id.contract_wage-((worked_days.payslip_id.contract_id.contract_wage/ days_count) * (days_to_remove if days_to_remove>0 else 1 or 1)) if worked_days.is_paid else 0
|
||||||
|
|
||||||
def _is_half_day(self):
|
def _is_half_day(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
#-*- coding:utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Time Off in Payslips',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Human Resources/Payroll',
|
||||||
|
'sequence': 95,
|
||||||
|
'depends': ['hr_work_entry_holidays', 'hr_payroll'],
|
||||||
|
'data': [
|
||||||
|
'security/hr_payroll_holidays_security.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/hr_leave_views.xml',
|
||||||
|
'views/hr_allocation_views.xml',
|
||||||
|
'views/hr_payslip_run_views.xml',
|
||||||
|
'views/hr_payslip_views.xml',
|
||||||
|
'data/mail_activity_data.xml',
|
||||||
|
'data/ir_actions_server_data.xml',
|
||||||
|
'data/hr_payroll_dashboard_warning_data.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'data/demo.xml',
|
||||||
|
],
|
||||||
|
'auto_install': True,
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'hr_payroll_holidays/static/src/**/*',
|
||||||
|
],
|
||||||
|
'web.assets_backend_lazy': [
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'license': 'OEEL-1',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hr_holidays.hr_holidays_cl_mit_2" model="hr.leave">
|
||||||
|
<field name="payslip_state">blocked</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_holidays.hr_holidays_sl_qdp" model="hr.leave">
|
||||||
|
<field name="payslip_state">blocked</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<record id="hr_payroll_dashboard_warning_leaves_to_defer" model="hr.payroll.dashboard.warning">
|
||||||
|
<field name="name">Time Off To Defer</field>
|
||||||
|
<field name="country_id" eval="False"/>
|
||||||
|
<field name="evaluation_code">
|
||||||
|
leaves_to_defer = self.env['hr.leave'].search([
|
||||||
|
('payslip_state', '=', 'blocked'),
|
||||||
|
('state', '=', 'validate'),
|
||||||
|
('employee_company_id', 'in', self.env.companies.ids),
|
||||||
|
])
|
||||||
|
if leaves_to_defer:
|
||||||
|
warning_count = len(leaves_to_defer)
|
||||||
|
warning_action = 'hr_payroll_holidays.hr_leave_action_open_to_defer'
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_payroll_dashboard_warning_leaves_no_document" model="hr.payroll.dashboard.warning">
|
||||||
|
<field name="name">Time Off Without Joined Document</field>
|
||||||
|
<field name="country_id" eval="False"/>
|
||||||
|
<field name="evaluation_code">
|
||||||
|
leaves_no_document = self.env['hr.leave'].search([
|
||||||
|
('state', 'not in', ['refuse', 'validate']),
|
||||||
|
('leave_type_support_document', '=', True),
|
||||||
|
('attachment_ids', '=', False),
|
||||||
|
('employee_company_id', 'in', self.env.companies.ids),
|
||||||
|
])
|
||||||
|
if leaves_no_document:
|
||||||
|
warning_count = len(leaves_no_document)
|
||||||
|
warning_records = leaves_no_document
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_payroll_dashboard_warning_leaves_no_allocation" model="hr.payroll.dashboard.warning">
|
||||||
|
<field name="name">Time Off Not Related To An Allocation</field>
|
||||||
|
<field name="country_id" eval="False"/>
|
||||||
|
<field name="evaluation_code">
|
||||||
|
leaves_no_allocation_ids = []
|
||||||
|
employees = self.env['hr.employee'].search([('company_id', 'in', self.env.companies.ids)])
|
||||||
|
consumed_leaves = employees._get_consumed_leaves(leave_types=self.env['hr.leave.type'].search([
|
||||||
|
('requires_allocation', '=', 'yes'),
|
||||||
|
('allows_negative', '=', False),
|
||||||
|
]))[1]
|
||||||
|
for employee in consumed_leaves:
|
||||||
|
to_recheck_leaves_per_leave_type = consumed_leaves[employee]
|
||||||
|
for holiday_status_id in to_recheck_leaves_per_leave_type:
|
||||||
|
for end_dates in to_recheck_leaves_per_leave_type[holiday_status_id]['excess_days']:
|
||||||
|
leave_id = to_recheck_leaves_per_leave_type[holiday_status_id]['excess_days'][end_dates]['leave_id']
|
||||||
|
leaves_no_allocation_ids.append(leave_id)
|
||||||
|
if leaves_no_allocation_ids:
|
||||||
|
warning_count = len(leaves_no_allocation_ids)
|
||||||
|
warning_records = self.env['hr.leave'].search([('id', 'in', leaves_no_allocation_ids)])
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="ir_actions_server_report_to_next_month" model="ir.actions.server">
|
||||||
|
<field name="name">Defer to Next Month</field>
|
||||||
|
<field name="model_id" ref="hr_holidays.model_hr_leave"/>
|
||||||
|
<field name="binding_model_id" ref="hr_holidays.model_hr_leave"/>
|
||||||
|
<field name="binding_view_types">list</field>
|
||||||
|
<field name="groups_id" eval="[(4, ref('hr_payroll.group_hr_payroll_user'))]"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
records.action_report_to_next_month()
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="ir_actions_server_mark_as_reported" model="ir.actions.server">
|
||||||
|
<field name="name">Mark as deferred</field>
|
||||||
|
<field name="model_id" ref="hr_holidays.model_hr_leave"/>
|
||||||
|
<field name="binding_model_id" ref="hr_holidays.model_hr_leave"/>
|
||||||
|
<field name="binding_view_types">list,form</field>
|
||||||
|
<field name="groups_id" eval="[(4, ref('hr_payroll.group_hr_payroll_user'))]"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
records.activity_feedback(['hr_payroll_holidays.mail_activity_data_hr_leave_to_defer'])
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="mail_activity_data_hr_leave_to_defer" model="mail.activity.type">
|
||||||
|
<field name="name">Leave to Defer</field>
|
||||||
|
<field name="icon">fa-plane</field>
|
||||||
|
<field name="sequence">100</field>
|
||||||
|
<field name="res_model">hr.leave</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
#-*- coding:utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import res_company
|
||||||
|
from . import hr_contract
|
||||||
|
from . import hr_leave
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import hr_payslip
|
||||||
|
from . import mail_activity
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
class HrContract(models.Model):
|
||||||
|
_inherit = 'hr.contract'
|
||||||
|
|
||||||
|
def _get_resource_calendar_leaves(self, start_dt, end_dt):
|
||||||
|
# prevent leaves that are associated with a blocked payslip to be taken
|
||||||
|
# into account, as they will be deferred lated.
|
||||||
|
return super()._get_resource_calendar_leaves(start_dt, end_dt).filtered(lambda l: l.holiday_id.payslip_state != 'blocked')
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.fields import Datetime
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tools.float_utils import float_compare
|
||||||
|
|
||||||
|
class HrLeave(models.Model):
|
||||||
|
_inherit = 'hr.leave'
|
||||||
|
|
||||||
|
payslip_state = fields.Selection([
|
||||||
|
('normal', 'To compute in next payslip'),
|
||||||
|
('done', 'Computed in current payslip'),
|
||||||
|
('blocked', 'To defer to next payslip')], string='Payslip State',
|
||||||
|
copy=False, default='normal', required=True, tracking=True)
|
||||||
|
|
||||||
|
def action_validate(self, check_state=True):
|
||||||
|
# Get employees payslips
|
||||||
|
all_payslips = self.env['hr.payslip'].sudo().search([
|
||||||
|
('employee_id', 'in', self.mapped('employee_id').ids),
|
||||||
|
('state', 'in', ['done', 'paid', 'verify']),
|
||||||
|
]).filtered(lambda p: p.is_regular)
|
||||||
|
done_payslips = all_payslips.filtered(lambda p: p.state in ['done', 'paid'])
|
||||||
|
waiting_payslips = all_payslips - done_payslips
|
||||||
|
# Mark Leaves to Defer
|
||||||
|
for leave in self:
|
||||||
|
if any(
|
||||||
|
payslip.employee_id == leave.employee_id \
|
||||||
|
and (payslip.date_from <= leave.date_to.date() \
|
||||||
|
and payslip.date_to >= leave.date_from.date()) for payslip in done_payslips) \
|
||||||
|
and not any(payslip.employee_id == leave.employee_id \
|
||||||
|
and (payslip.date_from <= leave.date_to.date() \
|
||||||
|
and payslip.date_to >= leave.date_from.date()) for payslip in waiting_payslips):
|
||||||
|
leave.payslip_state = 'blocked'
|
||||||
|
res = super().action_validate(check_state=check_state)
|
||||||
|
self.sudo()._recompute_payslips()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def action_refuse(self):
|
||||||
|
res = super().action_refuse()
|
||||||
|
self.sudo()._recompute_payslips()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _action_user_cancel(self, reason):
|
||||||
|
res = super()._action_user_cancel(reason)
|
||||||
|
self.sudo()._recompute_payslips()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _recompute_payslips(self):
|
||||||
|
# Recompute draft/waiting payslips
|
||||||
|
all_payslips = self.env['hr.payslip'].sudo().search([
|
||||||
|
('employee_id', 'in', self.mapped('employee_id').ids),
|
||||||
|
('state', 'in', ['draft', 'verify']),
|
||||||
|
]).filtered(lambda p: p.is_regular)
|
||||||
|
draft_payslips = self.env['hr.payslip']
|
||||||
|
waiting_payslips = self.env['hr.payslip']
|
||||||
|
for leave in self:
|
||||||
|
for payslip in all_payslips:
|
||||||
|
if payslip.employee_id == leave.employee_id and (payslip.date_from <= leave.date_to.date() and payslip.date_to >= leave.date_from.date()):
|
||||||
|
if payslip.state == 'draft':
|
||||||
|
draft_payslips |= payslip
|
||||||
|
elif payslip.state == 'verify':
|
||||||
|
waiting_payslips |= payslip
|
||||||
|
if draft_payslips:
|
||||||
|
draft_payslips._compute_worked_days_line_ids()
|
||||||
|
if waiting_payslips:
|
||||||
|
waiting_payslips.action_refresh_from_work_entries()
|
||||||
|
|
||||||
|
def _cancel_work_entry_conflict(self):
|
||||||
|
leaves_to_defer = self.filtered(lambda l: l.payslip_state == 'blocked')
|
||||||
|
for leave in leaves_to_defer:
|
||||||
|
leave.activity_schedule(
|
||||||
|
'hr_payroll_holidays.mail_activity_data_hr_leave_to_defer',
|
||||||
|
summary=_('Validated Time Off to Defer'),
|
||||||
|
note=_('Please create manually the work entry for %s',
|
||||||
|
leave.employee_id._get_html_link()),
|
||||||
|
user_id=leave.employee_id.company_id.deferred_time_off_manager.id or self.env.ref('base.user_admin').id)
|
||||||
|
return super(HrLeave, self - leaves_to_defer)._cancel_work_entry_conflict()
|
||||||
|
|
||||||
|
def activity_feedback(self, act_type_xmlids, user_id=None, feedback=None):
|
||||||
|
if 'hr_payroll_holidays.mail_activity_data_hr_leave_to_defer' in act_type_xmlids:
|
||||||
|
self.write({'payslip_state': 'done'})
|
||||||
|
return super().activity_feedback(act_type_xmlids, user_id=user_id, feedback=feedback)
|
||||||
|
|
||||||
|
def action_report_to_next_month(self):
|
||||||
|
for leave in self:
|
||||||
|
if not leave.employee_id or leave.payslip_state != 'blocked':
|
||||||
|
raise UserError(_('Only an employee time off to defer can be reported to next month'))
|
||||||
|
if (leave.date_to.year - leave.date_from.year) * 12 + leave.date_to.month - leave.date_from.month > 1:
|
||||||
|
raise UserError(_('The time off %s can not be reported because it is defined over more than 2 months', leave.display_name))
|
||||||
|
leave_work_entries = self.env['hr.work.entry'].search([
|
||||||
|
('employee_id', '=', leave.employee_id.id),
|
||||||
|
('company_id', '=', self.env.company.id),
|
||||||
|
('date_start', '>=', leave.date_from),
|
||||||
|
('date_stop', '<=', leave.date_to),
|
||||||
|
])
|
||||||
|
next_month_work_entries = self.env['hr.work.entry'].search([
|
||||||
|
('employee_id', '=', leave.employee_id.id),
|
||||||
|
('company_id', '=', self.env.company.id),
|
||||||
|
('state', '=', 'draft'),
|
||||||
|
('date_start', '>=', Datetime.to_datetime(leave.date_from + relativedelta(day=1, months=1))),
|
||||||
|
('date_stop', '<=', datetime.combine(Datetime.to_datetime(leave.date_to + relativedelta(day=31, months=1)), datetime.max.time()))
|
||||||
|
])
|
||||||
|
if not next_month_work_entries:
|
||||||
|
raise UserError(_('The next month work entries are not generated yet or are validated already for time off %s', leave.display_name))
|
||||||
|
if not leave_work_entries:
|
||||||
|
raise UserError(_('There is no work entries linked to this time off to report'))
|
||||||
|
for work_entry in leave_work_entries:
|
||||||
|
found = False
|
||||||
|
for next_work_entry in next_month_work_entries:
|
||||||
|
if next_work_entry.work_entry_type_id.code != "WORK100":
|
||||||
|
continue
|
||||||
|
if not float_compare(next_work_entry.duration, work_entry.duration, 2):
|
||||||
|
next_work_entry.work_entry_type_id = leave.holiday_status_id.work_entry_type_id
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
raise UserError(_('Not enough attendance work entries to report the time off %s. Please make the operation manually', leave.display_name))
|
||||||
|
# Should change payslip_state to 'done' at the same time
|
||||||
|
self.activity_feedback(['hr_payroll_holidays.mail_activity_data_hr_leave_to_defer'])
|
||||||
|
|
||||||
|
def _check_uncovered_by_validated_payslip(self):
|
||||||
|
payslips = self.env['hr.payslip'].sudo().search([
|
||||||
|
('employee_id', 'in', self.employee_id.ids),
|
||||||
|
('date_from', '<=', max(self.mapped('date_to'))),
|
||||||
|
('date_to', '>=', min(self.mapped('date_from'))),
|
||||||
|
('state', 'in', ['done', 'paid']),
|
||||||
|
])
|
||||||
|
|
||||||
|
for leave in self:
|
||||||
|
if any(
|
||||||
|
p.employee_id == leave.employee_id and
|
||||||
|
p.date_from <= leave.date_to.date() and
|
||||||
|
p.date_to >= leave.date_from.date() and
|
||||||
|
p.is_regular
|
||||||
|
for p in payslips
|
||||||
|
):
|
||||||
|
raise UserError(_("The pay of the month is already validated with this day included. If you need to adapt, please refer to HR."))
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if vals.get('active') and self._check_uncovered_by_validated_payslip():
|
||||||
|
self._check_uncovered_by_validated_payslip()
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
@api.ondelete(at_uninstall=False)
|
||||||
|
def _unlink_if_no_payslip(self):
|
||||||
|
self._check_uncovered_by_validated_payslip()
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, models, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslip(models.Model):
|
||||||
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
def compute_sheet(self):
|
||||||
|
if self.env.context.get('salary_simulation'):
|
||||||
|
return super().compute_sheet()
|
||||||
|
if self.filtered(lambda p: p.is_regular):
|
||||||
|
employees = self.mapped('employee_id')
|
||||||
|
leaves = self.env['hr.leave'].search([
|
||||||
|
('employee_id', 'in', employees.ids),
|
||||||
|
('state', '!=', 'refuse'),
|
||||||
|
])
|
||||||
|
leaves_to_defer = leaves.filtered(lambda l: l.payslip_state == 'blocked')
|
||||||
|
if leaves_to_defer:
|
||||||
|
raise ValidationError(_(
|
||||||
|
'There is some remaining time off to defer for these employees: \n\n %s',
|
||||||
|
', '.join(e.display_name for e in leaves_to_defer.mapped('employee_id'))))
|
||||||
|
dates = self.mapped('date_to')
|
||||||
|
max_date = datetime.combine(max(dates), datetime.max.time())
|
||||||
|
leaves_to_green = leaves.filtered(lambda l: l.payslip_state != 'blocked' and l.date_to <= max_date)
|
||||||
|
leaves_to_green.write({'payslip_state': 'done'})
|
||||||
|
return super().compute_sheet()
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class MailActivity(models.Model):
|
||||||
|
_inherit = 'mail.activity'
|
||||||
|
|
||||||
|
def _action_done(self, feedback=False, attachment_ids=None):
|
||||||
|
leave_activities = self.filtered(lambda act: act.res_model == 'hr.leave' and act.res_id)
|
||||||
|
if leave_activities:
|
||||||
|
type_to_defer_id = self.env['ir.model.data']._xmlid_to_res_id(
|
||||||
|
'hr_payroll_holidays.mail_activity_data_hr_leave_to_defer',
|
||||||
|
raise_if_not_found=False
|
||||||
|
)
|
||||||
|
if type_to_defer_id:
|
||||||
|
leave_activities = leave_activities.filtered(lambda act: act.activity_type_id.id == type_to_defer_id)
|
||||||
|
if leave_activities:
|
||||||
|
self.env['hr.leave'].browse(leave_activities.mapped('res_id')).write({'payslip_state': 'done'}) # done or normal??? to check
|
||||||
|
return super()._action_done(feedback=feedback, attachment_ids=attachment_ids)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = "res.company"
|
||||||
|
|
||||||
|
deferred_time_off_manager = fields.Many2one('res.users')
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
deferred_time_off_manager = fields.Many2one('res.users', related='company_id.deferred_time_off_manager', check_company=True, readonly=False)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<record id="hr_payroll.group_hr_payroll_user" model="res.groups">
|
||||||
|
<field name="implied_ids" eval="[(4, ref('hr_holidays.group_hr_holidays_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_payroll.group_hr_payroll_manager" model="res.groups">
|
||||||
|
<field name="implied_ids" eval="[(4, ref('hr_holidays.group_hr_holidays_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { TimeOffToDeferWarning, useTimeOffToDefer } from "@hr_payroll_holidays/views/hooks";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { FormController } from "@web/views/form/form_controller";
|
||||||
|
import { formView } from "@web/views/form/form_view";
|
||||||
|
|
||||||
|
export class PayslipFormController extends FormController {
|
||||||
|
static template = "hr_payroll_holidays.PayslipFormController";
|
||||||
|
static components = { ...PayslipFormController.components, TimeOffToDeferWarning };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
this.timeOff = useTimeOffToDefer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.category("views").add("hr_payslip_form", {
|
||||||
|
...formView,
|
||||||
|
Controller: PayslipFormController,
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
|
||||||
|
<t t-name="hr_payroll_holidays.PayslipFormController" t-inherit="web.FormView" t-inherit-mode="primary">
|
||||||
|
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="before">
|
||||||
|
<TimeOffToDeferWarning t-if="timeOff.hasTimeOffToDefer"/>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { TimeOffToDeferWarning, useTimeOffToDefer } from "@hr_payroll_holidays/views/hooks";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||||
|
import { listView } from "@web/views/list/list_view";
|
||||||
|
|
||||||
|
class PayslipListRenderer extends ListRenderer {
|
||||||
|
static template = "hr_payroll_holidays.PayslipListRenderer";
|
||||||
|
static components = { ...ListRenderer.components, TimeOffToDeferWarning };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
this.timeOff = useTimeOffToDefer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const PayslipListView = {
|
||||||
|
...listView,
|
||||||
|
Renderer: PayslipListRenderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("views").add("hr_payroll_payslip_tree", PayslipListView);
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
|
||||||
|
<t t-name="hr_payroll_holidays.PayslipListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary">
|
||||||
|
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
|
||||||
|
<TimeOffToDeferWarning t-if="timeOff.hasTimeOffToDefer"/>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Component, onWillStart } from "@odoo/owl";
|
||||||
|
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { user } from "@web/core/user";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export class TimeOffToDeferWarning extends Component {
|
||||||
|
static props = {};
|
||||||
|
static template = "hr_payroll_holidays.TimeOffToDeferWarning";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.actionService = useService("action");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {string} */
|
||||||
|
get timeOffButtonText() {
|
||||||
|
const [, before, inside, after] = _t(
|
||||||
|
"You have some <button>time off</button> to defer to the next month."
|
||||||
|
).match(/(.*)<button>(.*)<\/button>(.*)/) ?? [
|
||||||
|
"You have some",
|
||||||
|
"time off",
|
||||||
|
"to defer to the next month.",
|
||||||
|
];
|
||||||
|
return { before, inside, after };
|
||||||
|
}
|
||||||
|
|
||||||
|
onTimeOffToDefer() {
|
||||||
|
this.actionService.doAction("hr_payroll_holidays.hr_leave_action_open_to_defer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTimeOffToDefer() {
|
||||||
|
const orm = useService("orm");
|
||||||
|
const timeOff = {};
|
||||||
|
onWillStart(async () => {
|
||||||
|
const result = await orm.searchCount('hr.leave', [["payslip_state", "=", "blocked"], ["state", "=", "validate"], ["employee_company_id", "in", user.context.allowed_company_ids]]);
|
||||||
|
timeOff.hasTimeOffToDefer = result > 0;
|
||||||
|
});
|
||||||
|
return timeOff;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<templates>
|
||||||
|
<t t-name="hr_payroll_holidays.TimeOffToDeferWarning">
|
||||||
|
<div class="alert alert-warning text-center mb-0" role="alert">
|
||||||
|
<p class="mb-0">
|
||||||
|
<t t-esc="timeOffButtonText.before"/>
|
||||||
|
<button class="btn btn-link p-0 align-baseline o_open_defer_time_off" role="button" t-esc="timeOffButtonText.inside" t-on-click="onTimeOffToDefer"/>
|
||||||
|
<t t-esc="timeOffButtonText.after"/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { TimeOffToDeferWarning, useTimeOffToDefer } from "@hr_payroll_holidays/views/hooks";
|
||||||
|
import { WorkEntryCalendarController } from "@hr_work_entry_contract/views/work_entry_calendar/work_entry_calendar_controller";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(
|
||||||
|
WorkEntryCalendarController.prototype,
|
||||||
|
{
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this.timeOff = useTimeOffToDefer();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
patch(WorkEntryCalendarController, {
|
||||||
|
template: "hr_payroll_holidays.WorkEntryCalendarController",
|
||||||
|
components: { ...WorkEntryCalendarController.components, TimeOffToDeferWarning },
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
|
||||||
|
<t t-name="hr_payroll_holidays.WorkEntryCalendarController" t-inherit="web.CalendarController" t-inherit-mode="primary">
|
||||||
|
<xpath expr="//div[hasclass('o_calendar_container')]" position="before">
|
||||||
|
<TimeOffToDeferWarning t-if="timeOff.hasTimeOffToDefer"/>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="warning.time.off.to.defer">
|
||||||
|
<div class="alert alert-warning text-center mb-0" role="alert">
|
||||||
|
<p class="mb-0">
|
||||||
|
You have some <button class="btn btn-link p-0 o_open_defer_time_off" role="button">time off</button> to defer to the next month.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import common
|
||||||
|
from . import test_timeoff_defer
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo.fields import Date
|
||||||
|
from odoo.tests.common import TransactionCase, new_test_user
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
|
class TestPayrollHolidaysBase(TransactionCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.env.user.company_id.resource_calendar_id.tz = "Europe/Brussels"
|
||||||
|
cls.env.context = {'tz': 'Europe/Brussels'}
|
||||||
|
cls.dep_rd = cls.env['hr.department'].create({
|
||||||
|
'name': 'Research & Development - Test',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create employee
|
||||||
|
cls.vlad = new_test_user(cls.env, login='vlad', groups='base.group_user')
|
||||||
|
cls.emp = cls.env['hr.employee'].create({
|
||||||
|
'name': 'Donald',
|
||||||
|
'gender': 'male',
|
||||||
|
'birthday': '1946-06-14',
|
||||||
|
'department_id': cls.dep_rd.id,
|
||||||
|
'user_id': cls.vlad.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
cls.joseph = new_test_user(cls.env, login='joseph', groups='base.group_user,hr_holidays.group_hr_holidays_user')
|
||||||
|
|
||||||
|
cls.structure_type = cls.env['hr.payroll.structure.type'].create({
|
||||||
|
'name': 'Test - Developer',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create his contract
|
||||||
|
cls.env['hr.contract'].create({
|
||||||
|
'date_end': Date.today() + relativedelta(years=2),
|
||||||
|
'date_start': Date.to_date('2018-01-01'),
|
||||||
|
'name': 'Contract for Donald',
|
||||||
|
'wage': 5000.0,
|
||||||
|
'employee_id': cls.emp.id,
|
||||||
|
'structure_type_id': cls.structure_type.id,
|
||||||
|
'state': 'open',
|
||||||
|
})
|
||||||
|
|
||||||
|
cls.work_entry_type_unpaid = cls.env['hr.work.entry.type'].create({
|
||||||
|
'name': 'Unpaid Leave',
|
||||||
|
'is_leave': True,
|
||||||
|
'code': 'LEAVETEST300',
|
||||||
|
'round_days': 'HALF',
|
||||||
|
'round_days_type': 'DOWN',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a salary structure, necessary to compute sheet
|
||||||
|
cls.developer_pay_structure = cls.env['hr.payroll.structure'].create({
|
||||||
|
'name': 'Salary Structure for Software Developer',
|
||||||
|
'type_id': cls.structure_type.id,
|
||||||
|
'unpaid_work_entry_type_ids': [(4, cls.work_entry_type_unpaid.id, False)]
|
||||||
|
})
|
||||||
|
cls.structure_type.default_struct_id = cls.developer_pay_structure
|
||||||
|
|
||||||
|
# Create a leave type for our leaves
|
||||||
|
cls.leave_type = cls.env['hr.leave.type'].create({
|
||||||
|
'name': 'Unpaid leave',
|
||||||
|
'work_entry_type_id': cls.work_entry_type_unpaid.id,
|
||||||
|
'time_type': 'leave',
|
||||||
|
'requires_allocation': 'no',
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,386 @@
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from odoo.exceptions import ValidationError, UserError
|
||||||
|
from odoo.fields import Datetime
|
||||||
|
from odoo.tests.common import tagged
|
||||||
|
from odoo.addons.hr_payroll_holidays.tests.common import TestPayrollHolidaysBase
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestTimeoffDefer(TestPayrollHolidaysBase):
|
||||||
|
|
||||||
|
def test_no_defer(self):
|
||||||
|
#create payslip -> waiting or draft
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'Donald Payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Puts the payslip to draft/waiting
|
||||||
|
payslip.compute_sheet()
|
||||||
|
|
||||||
|
#create a time off for our employee, validating it now should not put it as to_defer
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Golf time',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': (date.today() + relativedelta(day=13)),
|
||||||
|
'request_date_to': (date.today() + relativedelta(day=16)),
|
||||||
|
})
|
||||||
|
leave.action_approve()
|
||||||
|
|
||||||
|
self.assertNotEqual(leave.payslip_state, 'blocked', 'Leave should not be to defer')
|
||||||
|
|
||||||
|
def test_to_defer(self):
|
||||||
|
#create payslip
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'Donald Payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Puts the payslip to draft/waiting
|
||||||
|
payslip.compute_sheet()
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
|
||||||
|
#create a time off for our employee, validating it now should put it as to_defer
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Golf time',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': (date.today() + relativedelta(day=13)),
|
||||||
|
'request_date_to': (date.today() + relativedelta(day=16)),
|
||||||
|
})
|
||||||
|
leave.action_approve()
|
||||||
|
self.assertEqual(leave.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
def test_multi_payslip_defer(self):
|
||||||
|
#A leave should only be set to defer if ALL colliding with the time period of the time off are in a done state
|
||||||
|
# it should not happen if a payslip for that time period is still in a waiting state
|
||||||
|
|
||||||
|
#create payslip -> waiting
|
||||||
|
waiting_payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'Donald Payslip draft',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
})
|
||||||
|
#payslip -> done
|
||||||
|
done_payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'Donald Payslip done',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Puts the waiting payslip to draft/waiting
|
||||||
|
waiting_payslip.compute_sheet()
|
||||||
|
# Puts the done payslip to the done state
|
||||||
|
done_payslip.compute_sheet()
|
||||||
|
done_payslip.action_payslip_done()
|
||||||
|
|
||||||
|
#create a time off for our employee, validating it now should not put it as to_defer
|
||||||
|
leave = self.env['hr.leave'].create({
|
||||||
|
'name': 'Golf time',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': (date.today() + relativedelta(day=13)),
|
||||||
|
'request_date_to': (date.today() + relativedelta(day=16)),
|
||||||
|
})
|
||||||
|
leave.action_approve()
|
||||||
|
|
||||||
|
self.assertNotEqual(leave.payslip_state, 'blocked', 'Leave should not be to defer')
|
||||||
|
|
||||||
|
def test_payslip_paid_past(self):
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'toto payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'date_from': '2022-01-01',
|
||||||
|
'date_to': '2022-01-31',
|
||||||
|
})
|
||||||
|
|
||||||
|
payslip.compute_sheet()
|
||||||
|
self.assertEqual(payslip.state, 'verify')
|
||||||
|
|
||||||
|
leave_1 = self.env['hr.leave'].with_user(self.vlad).create({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': '2022-01-12',
|
||||||
|
'request_date_to': '2022-01-12',
|
||||||
|
})
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
self.assertEqual(payslip.state, 'done')
|
||||||
|
|
||||||
|
leave_1.sudo().action_validate()
|
||||||
|
self.assertEqual(leave_1.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
# A Simple User can request a leave if a payslip is paid
|
||||||
|
leave_2 = self.env['hr.leave'].with_user(self.vlad).create({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': '2022-01-19',
|
||||||
|
'request_date_to': '2022-01-19',
|
||||||
|
})
|
||||||
|
leave_2.sudo().action_validate()
|
||||||
|
self.assertEqual(leave_2.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
# Check overlapping periods with no payslip
|
||||||
|
leave_3 = self.env['hr.leave'].with_user(self.vlad).create({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': '2022-01-31',
|
||||||
|
'request_date_to': '2022-02-01',
|
||||||
|
})
|
||||||
|
leave_3.sudo().action_validate()
|
||||||
|
self.assertEqual(leave_3.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
leave_4 = self.env['hr.leave'].with_user(self.vlad).create({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': '2021-01-31',
|
||||||
|
'request_date_to': '2022-01-03',
|
||||||
|
})
|
||||||
|
leave_4.sudo().action_validate()
|
||||||
|
self.assertEqual(leave_4.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
def test_report_to_next_month(self):
|
||||||
|
self.emp.contract_ids.generate_work_entries(date(2022, 1, 1), date(2022, 2, 28))
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'toto payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'date_from': '2022-01-01',
|
||||||
|
'date_to': '2022-01-31',
|
||||||
|
})
|
||||||
|
payslip.compute_sheet()
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
self.assertEqual(payslip.state, 'done')
|
||||||
|
|
||||||
|
leave = self.env['hr.leave'].new({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'request_date_from': date(2022, 1, 31),
|
||||||
|
'request_date_to': date(2022, 1, 31),
|
||||||
|
'request_hour_from': 7,
|
||||||
|
'request_hour_to': 18,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
leave = self.env['hr.leave'].create(leave._convert_to_write(leave._cache))
|
||||||
|
leave.action_validate()
|
||||||
|
self.assertEqual(leave.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
leave.action_report_to_next_month()
|
||||||
|
reported_work_entries = self.env['hr.work.entry'].search([
|
||||||
|
('employee_id', '=', self.emp.id),
|
||||||
|
('company_id', '=', self.env.company.id),
|
||||||
|
('state', '=', 'draft'),
|
||||||
|
('work_entry_type_id', '=', self.leave_type.work_entry_type_id.id),
|
||||||
|
('date_start', '>=', Datetime.to_datetime('2022-02-01')),
|
||||||
|
('date_stop', '<=', datetime.combine(Datetime.to_datetime('2022-02-28'), datetime.max.time()))
|
||||||
|
])
|
||||||
|
self.assertEqual(reported_work_entries[0].date_start, datetime(2022, 2, 1, 7, 0))
|
||||||
|
self.assertEqual(reported_work_entries[0].date_stop, datetime(2022, 2, 1, 11, 0))
|
||||||
|
self.assertEqual(reported_work_entries[1].date_start, datetime(2022, 2, 1, 12, 0))
|
||||||
|
self.assertEqual(reported_work_entries[1].date_stop, datetime(2022, 2, 1, 16, 0))
|
||||||
|
|
||||||
|
def test_report_to_next_month_overlap(self):
|
||||||
|
"""
|
||||||
|
If the time off overlap over 2 months, only report the exceeding part from january
|
||||||
|
In case leaves go over two months, only the leaves that are in the first month should be defered
|
||||||
|
"""
|
||||||
|
self.emp.contract_ids.generate_work_entries(date(2022, 1, 1), date(2022, 2, 28))
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'toto payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'date_from': '2022-01-01',
|
||||||
|
'date_to': '2022-01-31',
|
||||||
|
})
|
||||||
|
payslip.compute_sheet()
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
self.assertEqual(payslip.state, 'done')
|
||||||
|
|
||||||
|
leave = self.env['hr.leave'].new({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'request_date_from': date(2022, 1, 31),
|
||||||
|
'request_date_to': date(2022, 2, 2),
|
||||||
|
'request_hour_from': 7,
|
||||||
|
'request_hour_to': 18,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
leave = self.env['hr.leave'].create(leave._convert_to_write(leave._cache))
|
||||||
|
leave.action_validate()
|
||||||
|
self.assertEqual(leave.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
leave.action_report_to_next_month()
|
||||||
|
reported_work_entries = self.env['hr.work.entry'].search([
|
||||||
|
('employee_id', '=', self.emp.id),
|
||||||
|
('company_id', '=', self.env.company.id),
|
||||||
|
('state', '=', 'draft'),
|
||||||
|
('work_entry_type_id', '=', self.leave_type.work_entry_type_id.id),
|
||||||
|
('date_start', '>=', Datetime.to_datetime('2022-02-01')),
|
||||||
|
('date_stop', '<=', datetime.combine(Datetime.to_datetime('2022-02-28'), datetime.max.time()))
|
||||||
|
])
|
||||||
|
self.assertEqual(len(reported_work_entries), 6)
|
||||||
|
self.assertEqual(list({we.date_start.day for we in reported_work_entries}), [1, 2, 3])
|
||||||
|
self.assertEqual(reported_work_entries[0].date_start, datetime(2022, 2, 1, 7, 0))
|
||||||
|
self.assertEqual(reported_work_entries[0].date_stop, datetime(2022, 2, 1, 11, 0))
|
||||||
|
self.assertEqual(reported_work_entries[1].date_start, datetime(2022, 2, 1, 12, 0))
|
||||||
|
self.assertEqual(reported_work_entries[1].date_stop, datetime(2022, 2, 1, 16, 0))
|
||||||
|
|
||||||
|
def test_report_to_next_month_not_enough_days(self):
|
||||||
|
# If the time off contains too many days to be reported to next months, raise
|
||||||
|
self.emp.contract_ids.generate_work_entries(date(2022, 1, 1), date(2022, 2, 28))
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'toto payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'date_from': '2022-01-01',
|
||||||
|
'date_to': '2022-01-31',
|
||||||
|
})
|
||||||
|
payslip.compute_sheet()
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
self.assertEqual(payslip.state, 'done')
|
||||||
|
|
||||||
|
leave = self.env['hr.leave'].new({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'request_date_from': date(2022, 1, 1),
|
||||||
|
'request_date_to': date(2022, 1, 31),
|
||||||
|
'request_hour_from': 7,
|
||||||
|
'request_hour_to': 18,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
leave = self.env['hr.leave'].create(leave._convert_to_write(leave._cache))
|
||||||
|
leave.action_validate()
|
||||||
|
self.assertEqual(leave.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
leave.action_report_to_next_month()
|
||||||
|
|
||||||
|
def test_report_to_next_month_long_time_off(self):
|
||||||
|
# If the time off overlap over more than 2 months, raise
|
||||||
|
self.emp.contract_ids.generate_work_entries(date(2022, 1, 1), date(2022, 2, 28))
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'toto payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'date_from': '2022-01-01',
|
||||||
|
'date_to': '2022-01-31',
|
||||||
|
})
|
||||||
|
payslip.compute_sheet()
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
self.assertEqual(payslip.state, 'done')
|
||||||
|
|
||||||
|
leave = self.env['hr.leave'].new({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'request_date_from': date(2022, 1, 1),
|
||||||
|
'request_date_to': date(2022, 3, 10),
|
||||||
|
'request_hour_from': 7,
|
||||||
|
'request_hour_to': 18,
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
leave = self.env['hr.leave'].create(leave._convert_to_write(leave._cache))
|
||||||
|
leave.action_validate()
|
||||||
|
self.assertEqual(leave.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
leave.action_report_to_next_month()
|
||||||
|
|
||||||
|
def test_report_to_next_month_half_days(self):
|
||||||
|
self.leave_type.request_unit = 'half_day'
|
||||||
|
self.emp.contract_ids.generate_work_entries(date(2022, 1, 1), date(2022, 2, 28))
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'toto payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'date_from': '2022-01-01',
|
||||||
|
'date_to': '2022-01-31',
|
||||||
|
})
|
||||||
|
payslip.compute_sheet()
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
self.assertEqual(payslip.state, 'done')
|
||||||
|
|
||||||
|
leave = self.env['hr.leave'].new({
|
||||||
|
'name': 'Tennis',
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'request_date_from': date(2022, 1, 31),
|
||||||
|
'request_date_to': date(2022, 1, 31),
|
||||||
|
'request_unit_half': True,
|
||||||
|
'request_date_from_period': 'am',
|
||||||
|
})
|
||||||
|
leave._compute_date_from_to()
|
||||||
|
leave = self.env['hr.leave'].create(leave._convert_to_write(leave._cache))
|
||||||
|
|
||||||
|
leave.action_validate()
|
||||||
|
self.assertEqual(leave.payslip_state, 'blocked', 'Leave should be to defer')
|
||||||
|
|
||||||
|
leave.action_report_to_next_month()
|
||||||
|
reported_work_entries = self.env['hr.work.entry'].search([
|
||||||
|
('employee_id', '=', self.emp.id),
|
||||||
|
('company_id', '=', self.env.company.id),
|
||||||
|
('state', '=', 'draft'),
|
||||||
|
('work_entry_type_id', '=', self.leave_type.work_entry_type_id.id),
|
||||||
|
('date_start', '>=', Datetime.to_datetime('2022-02-01')),
|
||||||
|
('date_stop', '<=', datetime.combine(Datetime.to_datetime('2022-02-28'), datetime.max.time()))
|
||||||
|
])
|
||||||
|
self.assertEqual(len(reported_work_entries), 1)
|
||||||
|
self.assertEqual(reported_work_entries[0].date_start, datetime(2022, 2, 1, 7, 0))
|
||||||
|
self.assertEqual(reported_work_entries[0].date_stop, datetime(2022, 2, 1, 11, 0))
|
||||||
|
|
||||||
|
def test_defer_next_month_double_time_off(self):
|
||||||
|
"""
|
||||||
|
If you have a time off 5 days on Jun and 3 days on july, when you "defer it to next month"
|
||||||
|
it's only the 5 days of Jun that should be postponed to july.
|
||||||
|
"""
|
||||||
|
self.emp.contract_ids._generate_work_entries(datetime(2023, 6, 1), datetime(2023, 7, 31))
|
||||||
|
payslip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'toto payslip',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'date_from': '2023-06-01',
|
||||||
|
'date_to': '2023-06-30',
|
||||||
|
})
|
||||||
|
payslip.compute_sheet()
|
||||||
|
payslip.action_payslip_done()
|
||||||
|
self.assertEqual(payslip.state, 'done')
|
||||||
|
|
||||||
|
leave_data = [{
|
||||||
|
'name': 'Paid Time Off',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'request_date_from': '2023-06-26 00:00:00',
|
||||||
|
'request_date_to': '2023-06-30 23:59:59',
|
||||||
|
}, {
|
||||||
|
'name': 'Paid Time Off',
|
||||||
|
'employee_id': self.emp.id,
|
||||||
|
'holiday_status_id': self.leave_type.id,
|
||||||
|
'request_date_from': '2023-07-03 00:00:00',
|
||||||
|
'request_date_to': '2023-07-05 23:59:59',
|
||||||
|
}]
|
||||||
|
leaves = self.env['hr.leave'].create(leave_data)
|
||||||
|
leaves.action_validate()
|
||||||
|
leaves[0].action_report_to_next_month()
|
||||||
|
# reported work entries between the 1st of july 2023 to the 31st of july 2023
|
||||||
|
july_work_entries = self.env['hr.work.entry'].search([
|
||||||
|
('employee_id', '=', self.emp.id),
|
||||||
|
('company_id', '=', self.env.company.id),
|
||||||
|
('state', '=', 'draft'),
|
||||||
|
('work_entry_type_id', '=', self.leave_type.work_entry_type_id.id),
|
||||||
|
('date_start', '>=', Datetime.to_datetime('2023-07-01')),
|
||||||
|
('date_stop', '<=', datetime.combine(Datetime.to_datetime('2023-07-31'), datetime.max.time()))
|
||||||
|
])
|
||||||
|
# The length of reported work entries is 16 because we are generating records for 8 days of leave.
|
||||||
|
# Each day is divided into two parts, morning and afternoon, resulting in a total of 16 work entries.
|
||||||
|
# These leaves cover the period from July 3rd to July 12th, excluding July 1st and 2nd as they are designated holidays.
|
||||||
|
self.assertEqual(len(july_work_entries), 16)
|
||||||
|
self.assertEqual(list({we.date_start.day for we in july_work_entries}), [3, 4, 5, 6, 7, 10, 11, 12])
|
||||||
|
self.assertEqual(july_work_entries[0].date_start, datetime(2023, 7, 3, 6, 0))
|
||||||
|
self.assertEqual(july_work_entries[0].date_stop, datetime(2023, 7, 3, 10, 0))
|
||||||
|
self.assertEqual(july_work_entries[1].date_start, datetime(2023, 7, 3, 11, 0))
|
||||||
|
self.assertEqual(july_work_entries[1].date_stop, datetime(2023, 7, 3, 15, 0))
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_hr_leave_allocation_inherit_filter" model="ir.ui.view">
|
||||||
|
<field name="name">hr.view.leave.allocation.inherit.filter</field>
|
||||||
|
<field name="model">hr.leave.allocation</field>
|
||||||
|
<field name="inherit_id" ref="hr_holidays.view_hr_leave_allocation_filter"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='employee_id']" position="after">
|
||||||
|
<field name="employee_id" string="Employee Code" filter_domain="[('employee_id.registration_number','ilike', self)]"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hr_leave_view_search" model="ir.ui.view">
|
||||||
|
<field name="name">hr.leave.view.form.inherit.hr.payroll.holidays</field>
|
||||||
|
<field name="model">hr.leave</field>
|
||||||
|
<field name="inherit_id" ref="hr_holidays.hr_leave_view_search_manager"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//filter[@name='group_employee']" position="before">
|
||||||
|
<separator/>
|
||||||
|
<filter string="To Defer" name="to_defer" domain="[('payslip_state', '=', 'blocked')]" groups="hr_holidays.group_hr_holidays_user"/>
|
||||||
|
<separator/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='employee_id']" position="after">
|
||||||
|
<field name="employee_id" string="Employee Code" filter_domain="[('employee_id.registration_number','ilike', self)]"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_leave_action_open_to_defer" model="ir.actions.act_window">
|
||||||
|
<field name="name">Time Off to Defer</field>
|
||||||
|
<field name="res_model">hr.leave</field>
|
||||||
|
<field name="view_mode">list,kanban,form,calendar,activity</field>
|
||||||
|
<field name="search_view_id" ref="hr_holidays.hr_leave_view_search_manager"/>
|
||||||
|
<field name="domain">[('payslip_state', '=', 'blocked'), ('state', '=', 'validate'), ('employee_company_id', 'in', allowed_company_ids)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_leave_view_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">hr.leave.view.form.inherit</field>
|
||||||
|
<field name="model">hr.leave</field>
|
||||||
|
<field name="inherit_id" ref="hr_holidays.hr_leave_view_form_manager"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='holiday_status_id']" position="after">
|
||||||
|
<field name="payslip_state" widget="state_selection"
|
||||||
|
options="{'hide_label': False}" class="ms-auto mb-2"/>
|
||||||
|
</xpath>
|
||||||
|
<button name="action_refuse" position="before">
|
||||||
|
<button
|
||||||
|
string="Report to Next Month"
|
||||||
|
name="action_report_to_next_month"
|
||||||
|
type="object"
|
||||||
|
class="oe_highlight"
|
||||||
|
groups="hr_payroll.group_hr_payroll_user"
|
||||||
|
invisible="payslip_state != 'blocked' or state != 'validate'"/>
|
||||||
|
</button>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_leave_view_tree_inherit_payroll" model="ir.ui.view">
|
||||||
|
<field name="name">hr.holidays.view.list.inherit.work.entry</field>
|
||||||
|
<field name="model">hr.leave</field>
|
||||||
|
<field name="inherit_id" ref="hr_holidays.hr_leave_view_tree"/>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='state']" position="after">
|
||||||
|
<field name="payslip_state" widget="state_selection" options="{'hide_label': False}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_leave_work_entry_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Time Off</field>
|
||||||
|
<field name="res_model">hr.leave</field>
|
||||||
|
<field name="view_mode">list,kanban,form,calendar,activity</field>
|
||||||
|
<field name="search_view_id" ref="hr_holidays.hr_leave_view_search_manager"/>
|
||||||
|
<field name="view_id" ref="hr_payroll_holidays.hr_leave_view_tree_inherit_payroll"/>
|
||||||
|
<field name="context">{
|
||||||
|
'search_default_to_defer':1,
|
||||||
|
'hide_employee_name': 1
|
||||||
|
}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Meet the time off dashboard.
|
||||||
|
</p><p>
|
||||||
|
A great way to keep track on employee’s PTOs, sick days, and approval status.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_work_entry_leave_to_approve"
|
||||||
|
name="Time Off to Report"
|
||||||
|
action="hr_leave_work_entry_action"
|
||||||
|
parent="hr_payroll.menu_hr_payroll_work_entries_root"
|
||||||
|
sequence="75"
|
||||||
|
groups="hr_holidays.group_hr_holidays_user"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hr_payslip_run_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.run.view.list.inherit.hr.payroll.holidays</field>
|
||||||
|
<field name="model">hr.payslip.run</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//list" position="attributes">
|
||||||
|
<attribute name="js_class">hr_payroll_payslip_tree</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hr_payslip_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.view.form.inherit.hr.payroll.holidays</field>
|
||||||
|
<field name="model">hr.payslip</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form" position="attributes">
|
||||||
|
<attribute name="js_class">hr_payslip_form</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="hr_payslip_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">hr.payslip.view.list.inherit.hr.payroll.holidays</field>
|
||||||
|
<field name="model">hr.payslip</field>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//list" position="attributes">
|
||||||
|
<attribute name="js_class">hr_payroll_payslip_tree</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.hr.payroll.holidays</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="priority" eval="45"/>
|
||||||
|
<field name="inherit_id" ref="hr_payroll.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//block[@id='hr_payroll_accountant']" position="after">
|
||||||
|
<block title="Time Off">
|
||||||
|
<setting string="Deferred Time Off" help="Postpone time off after payslip validation">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16 ms-2">
|
||||||
|
<label string="Responsible" for="deferred_time_off_manager"
|
||||||
|
class="col-md-6 p-0 m-0 o_light_label"/>
|
||||||
|
<field name="deferred_time_off_manager" class="col-lg-6 p-0"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue