forked from Mapan/odoo17e
1400 lines
75 KiB
Python
1400 lines
75 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from pytz import timezone
|
|
from dateutil.relativedelta import relativedelta, MO, SU
|
|
from dateutil import rrule
|
|
from collections import defaultdict
|
|
from datetime import date, datetime, timedelta
|
|
|
|
from odoo import api, models, fields, _
|
|
from odoo.tools import float_round, date_utils, ormcache
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class Payslip(models.Model):
|
|
_inherit = 'hr.payslip'
|
|
|
|
meal_voucher_count = fields.Integer(
|
|
compute='_compute_work_entry_dependent_benefits') # Overrides compute method
|
|
private_car_missing_days = fields.Integer(
|
|
string='Days Not Granting Private Car Reimbursement',
|
|
compute='_compute_work_entry_dependent_benefits')
|
|
representation_fees_missing_days = fields.Integer(
|
|
string='Days Not Granting Representation Fees',
|
|
compute='_compute_work_entry_dependent_benefits')
|
|
l10n_be_is_double_pay = fields.Boolean(compute='_compute_l10n_be_is_double_pay')
|
|
l10n_be_max_seizable_amount = fields.Float(compute='_compute_l10n_be_max_seizable_amount')
|
|
l10n_be_max_seizable_warning = fields.Char(compute='_compute_l10n_be_max_seizable_amount')
|
|
l10n_be_is_december = fields.Boolean(compute='_compute_l10n_be_is_december')
|
|
l10n_be_has_eco_vouchers = fields.Boolean(compute='_compute_l10n_be_has_eco_vouchers', search='_search_l10n_be_has_eco_vouchers')
|
|
|
|
@api.depends('employee_id', 'contract_id', 'struct_id', 'date_from', 'date_to')
|
|
def _compute_input_line_ids(self):
|
|
res = super()._compute_input_line_ids()
|
|
for slip in self:
|
|
if not slip.employee_id or not slip.date_from or not slip.date_to:
|
|
continue
|
|
if slip.struct_id.code == 'CP200WARRANT':
|
|
months = relativedelta(date_utils.add(slip.date_to, days=1), slip.date_from).months
|
|
if slip.employee_id.id in self.env.context.get('commission_real_values', {}):
|
|
warrant_value = self.env.context['commission_real_values'][slip.employee_id.id]
|
|
else:
|
|
warrant_value = slip.contract_id.commission_on_target * months
|
|
warrant_type = self.env.ref('l10n_be_hr_payroll.cp200_other_input_warrant')
|
|
lines_to_remove = slip.input_line_ids.filtered(lambda x: x.input_type_id == warrant_type)
|
|
to_remove_vals = [(3, line.id, False) for line in lines_to_remove]
|
|
to_add_vals = [(0, 0, {
|
|
'amount': warrant_value,
|
|
'input_type_id': self.env.ref('l10n_be_hr_payroll.cp200_other_input_warrant').id
|
|
})]
|
|
input_line_vals = to_remove_vals + to_add_vals
|
|
slip.update({'input_line_ids': input_line_vals})
|
|
# If a double holiday pay should be recovered
|
|
elif slip.struct_id.code == 'CP200DOUBLE':
|
|
to_recover = slip._get_sum_european_time_off_days()
|
|
if to_recover:
|
|
european_type = self.env.ref('l10n_be_hr_payroll.input_double_holiday_european_leave_deduction')
|
|
lines_to_remove = slip.input_line_ids.filtered(lambda x: x.input_type_id == european_type)
|
|
to_remove_vals = [(3, line.id, False) for line in lines_to_remove]
|
|
to_add_vals = [(0, 0, {
|
|
'name': _('European Leaves Deduction'),
|
|
'amount': to_recover,
|
|
'input_type_id': european_type.id,
|
|
})]
|
|
slip.write({'input_line_ids': to_remove_vals + to_add_vals})
|
|
return res
|
|
|
|
@ormcache('self.employee_id', 'self.date_from', 'self.date_to')
|
|
def _get_period_contracts(self):
|
|
# Returns all the employee contracts over the same payslip period, to avoid
|
|
# double remunerations for some line codes
|
|
self.ensure_one()
|
|
if self.env.context.get('salary_simulation'):
|
|
return self.env.context['origin_contract_id']
|
|
contracts = self.employee_id._get_contracts(
|
|
self.date_from,
|
|
self.date_to,
|
|
states=['open', 'close']
|
|
).sorted('date_start')
|
|
return contracts.ids
|
|
|
|
@api.depends('worked_days_line_ids.number_of_hours', 'worked_days_line_ids.is_paid', 'worked_days_line_ids.is_credit_time')
|
|
def _compute_worked_hours(self):
|
|
super()._compute_worked_hours()
|
|
for payslip in self:
|
|
payslip.sum_worked_hours -= sum([line.number_of_hours for line in payslip.worked_days_line_ids if line.is_credit_time])
|
|
|
|
@api.depends('struct_id', 'date_from')
|
|
def _compute_l10n_be_is_december(self):
|
|
for payslip in self:
|
|
payslip.l10n_be_is_december = payslip.struct_id.code == "CP200MONTHLY" and payslip.date_from and payslip.date_from.month == 12
|
|
|
|
def _compute_work_entry_dependent_benefits(self):
|
|
if self.env.context.get('salary_simulation'):
|
|
for payslip in self:
|
|
payslip.meal_voucher_count = 20
|
|
payslip.private_car_missing_days = 0
|
|
payslip.representation_fees_missing_days = 0
|
|
else:
|
|
all_benefits = self.env['hr.work.entry.type'].get_work_entry_type_benefits()
|
|
query = self.env['l10n_be.work.entry.daily.benefit.report']._search([
|
|
('employee_id', 'in', self.mapped('employee_id').ids),
|
|
('day', '<=', max(self.mapped('date_to'))),
|
|
('day', '>=', min(self.mapped('date_from'))),
|
|
])
|
|
query_str, params = query.select('day', 'benefit_name', 'employee_id')
|
|
self.env.cr.execute(query_str, params)
|
|
work_entries_benefits_rights = self.env.cr.dictfetchall()
|
|
|
|
work_entries_benefits_rights_by_employee = defaultdict(list)
|
|
for work_entries_benefits_right in work_entries_benefits_rights:
|
|
employee_id = work_entries_benefits_right['employee_id']
|
|
work_entries_benefits_rights_by_employee[employee_id].append(work_entries_benefits_right)
|
|
|
|
# {(calendar, date_from, date_to): resources}
|
|
mapped_resources = defaultdict(lambda: self.env['resource.resource'])
|
|
for payslip in self:
|
|
contract = payslip.contract_id
|
|
calendar = contract.resource_calendar_id if not contract.time_credit else contract.standard_calendar_id
|
|
mapped_resources[(calendar, payslip.date_from, payslip.date_to)] |= contract.employee_id.resource_id
|
|
# {(calendar, date_from, date_to): intervals}}
|
|
mapped_intervals = {}
|
|
for (calendar, date_from, date_to), resources in mapped_resources.items():
|
|
tz = timezone(calendar.tz)
|
|
mapped_intervals[(calendar, date_from, date_to)] = calendar._attendance_intervals_batch(
|
|
tz.localize(fields.Datetime.to_datetime(date_from)),
|
|
tz.localize(fields.Datetime.to_datetime(date_to) + timedelta(days=1, seconds=-1)),
|
|
resources=resources, tz=tz)
|
|
|
|
for payslip in self:
|
|
contract = payslip.contract_id
|
|
benefits = dict.fromkeys(all_benefits, 0)
|
|
date_from = max(payslip.date_from, contract.date_start)
|
|
date_to = min(payslip.date_to, contract.date_end or payslip.date_to)
|
|
for work_entries_benefits_right in (
|
|
work_entries_benefits_right
|
|
for work_entries_benefits_right in work_entries_benefits_rights_by_employee[payslip.employee_id.id]
|
|
if date_from <= work_entries_benefits_right['day'] <= date_to
|
|
):
|
|
if work_entries_benefits_right['benefit_name'] not in benefits:
|
|
benefits[work_entries_benefits_right['benefit_name']] = 1
|
|
else:
|
|
benefits[work_entries_benefits_right['benefit_name']] += 1
|
|
|
|
contract = payslip.contract_id
|
|
resource = contract.employee_id.resource_id
|
|
calendar = contract.resource_calendar_id if not contract.time_credit else contract.standard_calendar_id
|
|
intervals = mapped_intervals[(calendar, payslip.date_from, payslip.date_to)][resource.id]
|
|
|
|
nb_of_days_to_work = len({dt_from.date(): True for (dt_from, dt_to, attendance) in intervals})
|
|
payslip.private_car_missing_days = nb_of_days_to_work - (benefits['private_car'] if 'private_car' in benefits else 0)
|
|
payslip.representation_fees_missing_days = nb_of_days_to_work - (benefits['representation_fees'] if 'representation_fees' in benefits else 0)
|
|
payslip.meal_voucher_count = benefits['meal_voucher']
|
|
|
|
@api.depends('struct_id')
|
|
def _compute_l10n_be_is_double_pay(self):
|
|
for payslip in self:
|
|
payslip.l10n_be_is_double_pay = payslip.struct_id.code == "CP200DOUBLE"
|
|
|
|
@api.depends('input_line_ids')
|
|
def _compute_l10n_be_has_eco_vouchers(self):
|
|
for slip in self:
|
|
slip.l10n_be_has_eco_vouchers = any(input_line.code == 'ECOVOUCHERS' for input_line in slip.input_line_ids)
|
|
|
|
def _search_l10n_be_has_eco_vouchers(self, operator, value):
|
|
if operator not in ['=', '!='] or not isinstance(value, bool):
|
|
raise UserError(_('Operation not supported'))
|
|
if operator != '=':
|
|
value = not value
|
|
self._cr.execute("""
|
|
SELECT id
|
|
FROM hr_payslip payslip
|
|
WHERE EXISTS
|
|
(SELECT 1
|
|
FROM hr_payslip_input hpi
|
|
JOIN hr_payslip_input_type hpit
|
|
ON hpi.input_type_id = hpit.id AND hpit.code = 'ECOVOUCHERS'
|
|
WHERE hpi.payslip_id = payslip.id
|
|
LIMIT 1)
|
|
""")
|
|
return [('id', 'in' if value else 'not in', [r[0] for r in self._cr.fetchall()])]
|
|
|
|
@api.depends('struct_id')
|
|
def _compute_contract_domain_ids(self):
|
|
reimbursement_payslips = self.filtered(lambda p: p.struct_id.code == "CP200REIMBURSEMENT")
|
|
for payslip in reimbursement_payslips:
|
|
payslip.contract_domain_ids = self.env['hr.contract'].search([
|
|
('company_id', '=', payslip.company_id.id),
|
|
('employee_id', '=', payslip.employee_id.id),
|
|
('state', '!=', 'cancel')])
|
|
super(Payslip, self - reimbursement_payslips)._compute_contract_domain_ids()
|
|
|
|
@api.depends('date_to', 'line_ids.total', 'input_line_ids.code')
|
|
def _compute_l10n_be_max_seizable_amount(self):
|
|
# Source: https://emploi.belgique.be/fr/themes/remuneration/protection-de-la-remuneration/saisie-et-cession-sur-salaires
|
|
all_payslips = self.env['hr.payslip'].search([
|
|
('employee_id', 'in', self.employee_id.ids),
|
|
('state', '!=', 'cancel')])
|
|
payslip_values = all_payslips._get_line_values(['NET'])
|
|
for payslip in self:
|
|
if payslip.struct_id.country_id.code != 'BE':
|
|
payslip.l10n_be_max_seizable_amount = 0
|
|
payslip.l10n_be_max_seizable_warning = False
|
|
continue
|
|
|
|
rates = self.env['hr.rule.parameter']._get_parameter_from_code('cp200_seizable_percentages', payslip.date_to, raise_if_not_found=False)
|
|
child_increase = self.env['hr.rule.parameter']._get_parameter_from_code('cp200_seizable_amount_child', payslip.date_to, raise_if_not_found=False)
|
|
if not rates or not child_increase:
|
|
payslip.l10n_be_max_seizable_amount = 0
|
|
payslip.l10n_be_max_seizable_warning = False
|
|
continue
|
|
|
|
# Note: the ceiling amounts are based on the net revenues
|
|
period_payslips = all_payslips.filtered(
|
|
lambda p: p.employee_id == payslip.employee_id and p.date_from == payslip.date_from and p.date_to == payslip.date_to)
|
|
net_amount = sum([payslip_values['NET'][p.id]['total'] for p in period_payslips])
|
|
seized_amount = sum([period_payslips._get_input_line_amount(code) for code in ['ATTACH_SALARY', 'ASSIG_SALARY', 'CHILD_SUPPORT']])
|
|
net_amount += seized_amount
|
|
# Note: The reduction for dependant children is not applied most of the time because
|
|
# the process is too complex.
|
|
# To benefit from this increase in the elusive or non-transferable quotas, the worker
|
|
# whose remuneration is subject to seizure or transfer, must declare it using a form,
|
|
# the model of which has been published in the Belgian Official Gazette. of 30 November
|
|
# 2006.
|
|
# He must attach to this form the documents establishing the reality of the
|
|
# charge invoked.
|
|
# Source: Opinion on the indexation of the amounts set in Article 1, paragraph 4, of
|
|
# the Royal Decree of 27 December 2004 implementing Articles 1409, § 1, paragraph 4,
|
|
# and 1409, § 1 bis, paragraph 4 , of the Judicial Code relating to the limitation of
|
|
# seizure when there are dependent children, MB, December 13, 2019.
|
|
dependent_children = payslip.employee_id.l10n_be_dependent_children_attachment
|
|
max_seizable_amount = 0
|
|
for left, right, rate in rates:
|
|
if dependent_children:
|
|
left += dependent_children * child_increase
|
|
right += dependent_children * child_increase
|
|
if left <= net_amount:
|
|
max_seizable_amount += (min(net_amount, right) - left) * rate
|
|
payslip.l10n_be_max_seizable_amount = max_seizable_amount
|
|
if max_seizable_amount and seized_amount > max_seizable_amount:
|
|
payslip.l10n_be_max_seizable_warning = _('The seized amount (%s€) is above the belgian ceilings. Given a global net salary of %s€ for the pay period and %s dependent children, the maximum seizable amount is equal to %s€', round(seized_amount, 2), round(net_amount, 2), round(dependent_children, 2), round(max_seizable_amount, 2))
|
|
else:
|
|
payslip.l10n_be_max_seizable_warning = False
|
|
|
|
def _get_worked_day_lines_hours_per_day(self):
|
|
self.ensure_one()
|
|
if self.contract_id.time_credit:
|
|
return self.contract_id.standard_calendar_id.hours_per_day
|
|
return super()._get_worked_day_lines_hours_per_day()
|
|
|
|
def _get_worked_day_lines_values(self, domain=None):
|
|
self.ensure_one()
|
|
res = []
|
|
if self.struct_id.country_id.code != 'BE':
|
|
return super()._get_worked_day_lines_values(domain=domain)
|
|
# If a belgian payslip has half-day attendances/time off, it the worked days lines should
|
|
# be separated
|
|
work_hours = self.contract_id._get_work_hours_split_half(self.date_from, self.date_to, domain=domain)
|
|
work_hours_ordered = sorted(work_hours.items(), key=lambda x: x[1])
|
|
for worked_days_data, duration_data in work_hours_ordered:
|
|
duration_type, work_entry_type_id = worked_days_data
|
|
number_of_days, number_of_hours = duration_data
|
|
work_entry_type = self.env['hr.work.entry.type'].browse(work_entry_type_id)
|
|
attendance_line = {
|
|
'sequence': work_entry_type.sequence,
|
|
'work_entry_type_id': work_entry_type_id,
|
|
'number_of_days': number_of_days,
|
|
'number_of_hours': number_of_hours,
|
|
}
|
|
res.append(attendance_line)
|
|
# If there is a public holiday less than 30 days after the end of the contract
|
|
# this public holiday should be taken into account in the worked days lines
|
|
if self.contract_id.date_end and self.date_from <= self.contract_id.date_end <= self.date_to:
|
|
# If the contract is followed by another one (eg. after an appraisal)
|
|
if self.contract_id.employee_id.contract_ids.filtered(lambda c: c.state in ['open', 'close'] and c.date_start > self.contract_id.date_end):
|
|
return res
|
|
public_holiday_type = self.env.ref('l10n_be_hr_payroll.work_entry_type_bank_holiday')
|
|
public_leaves = self.contract_id.resource_calendar_id.global_leave_ids.filtered(
|
|
lambda l: l.work_entry_type_id == public_holiday_type)
|
|
# If less than 15 days under contract, the public holidays is not reimbursed
|
|
public_leaves = public_leaves.filtered(
|
|
lambda l: (l.date_from.date() - self.employee_id.first_contract_date).days >= 15)
|
|
# If less than 15 days of occupation -> no payment of the time off after contract
|
|
# If less than 1 month of occupation -> payment of the time off occurring within 15 days after contract.
|
|
# Occupation = duration since the start of the contract, from date to date
|
|
public_leaves = public_leaves.filtered(
|
|
lambda l: 0 < (l.date_from.date() - self.contract_id.date_end).days <= (30 if self.employee_id.first_contract_date + relativedelta(months=1) <= self.contract_id.date_end else 15))
|
|
if public_leaves:
|
|
input_type_id = self.env.ref('l10n_be_hr_payroll.cp200_other_input_after_contract_public_holidays').id
|
|
if input_type_id not in self.input_line_ids.mapped('input_type_id').ids:
|
|
self.write({'input_line_ids': [(0, 0, {
|
|
'name': _('After Contract Public Holidays'),
|
|
'amount': 0.0,
|
|
'input_type_id': self.env.ref('l10n_be_hr_payroll.cp200_other_input_after_contract_public_holidays').id,
|
|
})]})
|
|
# Handle loss on commissions
|
|
if self._get_last_year_average_variable_revenues():
|
|
we_types_ids = (
|
|
self.env.ref('l10n_be_hr_payroll.work_entry_type_bank_holiday') + self.env.ref('l10n_be_hr_payroll.work_entry_type_small_unemployment')
|
|
).ids
|
|
# if self.worked_days_line_ids.filtered(lambda wd: wd.code in ['LEAVE205', 'LEAVE500']):
|
|
if any(line_vals['work_entry_type_id'] in we_types_ids for line_vals in res):
|
|
we_type = self.env.ref('l10n_be_hr_payroll.work_entry_type_simple_holiday_pay_variable_salary')
|
|
res.append({
|
|
'sequence': we_type.sequence,
|
|
'work_entry_type_id': we_type.id,
|
|
'number_of_days': 0,
|
|
'number_of_hours': 0,
|
|
})
|
|
return res
|
|
|
|
def _get_last_year_average_variable_revenues(self):
|
|
if not self.contract_id.commission_on_target:
|
|
return 0
|
|
date_from = self.env.context.get('variable_revenue_date_from', self.date_from)
|
|
first_contract_date = self.employee_id.first_contract_date
|
|
if not first_contract_date:
|
|
return 0
|
|
start = first_contract_date
|
|
end = date_from + relativedelta(day=31, months=-1)
|
|
number_of_month = (end.year - start.year) * 12 + (end.month - start.month) + 1
|
|
number_of_month = min(12, number_of_month)
|
|
if number_of_month <= 0:
|
|
return 0
|
|
payslips = self.env['hr.payslip'].search([
|
|
('employee_id', '=', self.employee_id.id),
|
|
('state', 'in', ['done', 'paid']),
|
|
('date_from', '>=', date_from + relativedelta(months=-12, day=1)),
|
|
('date_from', '<=', date_from),
|
|
], order="date_from asc")
|
|
total_amount = payslips._get_line_values(['COMMISSION'], compute_sum=True)['COMMISSION']['sum']['total']
|
|
return total_amount / number_of_month if number_of_month else 0
|
|
|
|
def _get_last_year_average_warrant_revenues(self):
|
|
warrant_payslips = self.env['hr.payslip'].search([
|
|
('employee_id', '=', self.employee_id.id),
|
|
('state', 'in', ['done', 'paid']),
|
|
('struct_id.code', '=', 'CP200WARRANT'),
|
|
('date_from', '>=', self.date_from + relativedelta(months=-12, day=1)),
|
|
('date_from', '<', self.date_from),
|
|
], order="date_from asc")
|
|
total_amount = warrant_payslips._get_line_values(['BASIC'], compute_sum=True)['BASIC']['sum']['total']
|
|
first_contract_date = self.employee_id.first_contract_date
|
|
if not first_contract_date:
|
|
return 0
|
|
# Only complete months count
|
|
if first_contract_date.day != 1:
|
|
start = first_contract_date + relativedelta(day=1, months=1)
|
|
else:
|
|
start = first_contract_date
|
|
end = self.date_from + relativedelta(day=31, months=-1)
|
|
number_of_month = (end.year - start.year) * 12 + (end.month - start.month) + 1
|
|
number_of_month = min(12, number_of_month)
|
|
return total_amount / number_of_month if number_of_month else 0
|
|
|
|
def _compute_number_complete_months_of_work(self, date_from, date_to, contracts):
|
|
invalid_days_by_year = defaultdict(lambda: defaultdict(dict))
|
|
for day in rrule.rrule(rrule.DAILY, dtstart=date_from + relativedelta(day=1), until=date_to + relativedelta(day=31)):
|
|
invalid_days_by_year[day.year][day.month][day.date()] = True
|
|
|
|
public_holidays = [(leave.date_from.date(), leave.date_to.date()) for leave in self.employee_id._get_public_holidays(date_from, date_to)]
|
|
for contract in contracts:
|
|
work_days = {int(d) for d in contract.resource_calendar_id._get_global_attendances().mapped('dayofweek')}
|
|
|
|
previous_week_start = max(contract.date_start + relativedelta(weeks=-1, weekday=MO(-1)), date_from + relativedelta(day=1))
|
|
next_week_end = min(contract.date_end + relativedelta(weeks=+1, weekday=SU(+1)) if contract.date_end else date.max, date_to)
|
|
days_to_check = rrule.rrule(rrule.DAILY, dtstart=previous_week_start, until=next_week_end)
|
|
for day in days_to_check:
|
|
day = day.date()
|
|
out_of_schedule = True
|
|
|
|
# Full time credit time doesn't count
|
|
if contract.time_credit and not contract.work_time_rate:
|
|
continue
|
|
if (contract.date_start <= day <= (contract.date_end or date.max) or
|
|
day.weekday() not in work_days or
|
|
any(date_from <= day <= date_to for date_from, date_to in public_holidays)):
|
|
out_of_schedule = False
|
|
invalid_days_by_year[day.year][day.month][day] &= out_of_schedule
|
|
|
|
complete_months = [
|
|
month
|
|
for year, invalid_days_by_months in invalid_days_by_year.items()
|
|
for month, days in invalid_days_by_months.items()
|
|
if not any(days.values())
|
|
]
|
|
return len(complete_months)
|
|
|
|
def _compute_presence_prorata(self, date_from, date_to, contracts):
|
|
unpaid_work_entry_types = self.struct_id.unpaid_work_entry_type_ids
|
|
paid_work_entry_types = self.env['hr.work.entry.type'].search([]) - unpaid_work_entry_types
|
|
hours = contracts.get_work_hours(date_from, date_to)
|
|
paid_hours = sum(v for k, v in hours.items() if k in paid_work_entry_types.ids)
|
|
unpaid_hours = sum(v for k, v in hours.items() if k in unpaid_work_entry_types.ids)
|
|
# Take 30 unpaid sick open days as paid time off
|
|
if self.struct_id.code == 'CP200THIRTEEN':
|
|
unpaid_sick_codes = ['LEAVE280', 'LEAVE214']
|
|
date_from = datetime.combine(date_from, datetime.min.time())
|
|
date_to = datetime.combine(date_to, datetime.max.time())
|
|
work_entries = self.env['hr.work.entry'].search([
|
|
('state', 'in', ['validated', 'draft']),
|
|
('employee_id', '=', self.employee_id.id),
|
|
('date_start', '>=', date_from),
|
|
('date_stop', '<=', date_to),
|
|
('work_entry_type_id.code', 'in', unpaid_sick_codes),
|
|
], order="date_start asc")
|
|
days_count, valid_sick_hours = 0, 0
|
|
valid_days = set()
|
|
for work_entry in work_entries:
|
|
work_entry_date = work_entry.date_start.date()
|
|
if work_entry_date in valid_days:
|
|
valid_sick_hours += work_entry.duration
|
|
elif days_count < 30:
|
|
valid_days.add(work_entry_date)
|
|
days_count += 1
|
|
valid_sick_hours += work_entry.duration
|
|
paid_hours += valid_sick_hours
|
|
unpaid_hours -= valid_sick_hours
|
|
return paid_hours / (paid_hours + unpaid_hours) if paid_hours or unpaid_hours else 0
|
|
|
|
def _get_paid_amount_13th_month(self):
|
|
# Counts the number of fully worked month
|
|
# If any day in the month is not covered by the contract dates coverage
|
|
# the entire month is not taken into account for the proratization
|
|
contracts = self.employee_id.contract_ids.filtered(lambda c: c.state not in ['draft', 'cancel'] and c.structure_type_id == self.struct_id.type_id)
|
|
first_contract_date = self.contract_id.employee_id._get_first_contract_date(no_gap=False)
|
|
if not contracts or not first_contract_date:
|
|
return 0.0
|
|
# Only employee with at least 6 months of XP can benefit from the 13th month bonus
|
|
# aka employee who started before the 7th of July (to avoid issues when the month starts
|
|
# with holidays / week-ends, etc)
|
|
if first_contract_date.year == self.date_from.year and \
|
|
((first_contract_date.month == 7 and first_contract_date.day > 7) \
|
|
or (first_contract_date.month > 7)):
|
|
return 0.0
|
|
|
|
date_from = max(first_contract_date, self.date_from + relativedelta(day=1, month=1))
|
|
date_to = self.date_to + relativedelta(day=31)
|
|
|
|
basic = self.contract_id._get_contract_wage()
|
|
|
|
force_months = self.input_line_ids.filtered(lambda l: l.code == 'MONTHS')
|
|
if force_months:
|
|
n_months = force_months[0].amount
|
|
presence_prorata = 1
|
|
else:
|
|
# 1. Number of months
|
|
n_months = min(12, self._compute_number_complete_months_of_work(date_from, date_to, contracts))
|
|
# 2. Deduct absences
|
|
presence_prorata = self._compute_presence_prorata(date_from, date_to, contracts)
|
|
|
|
# Could happen for contracts with gaps
|
|
if n_months < 6:
|
|
return 0.0
|
|
|
|
fixed_salary = basic * n_months / 12 * presence_prorata
|
|
|
|
force_avg_variable_revenues = self.input_line_ids.filtered(lambda l: l.code == 'VARIABLE')
|
|
if force_avg_variable_revenues:
|
|
avg_variable_revenues = force_avg_variable_revenues[0].amount
|
|
else:
|
|
if not n_months:
|
|
avg_variable_revenues = 0
|
|
else:
|
|
avg_variable_revenues = self.with_context(
|
|
variable_revenue_date_from=self.date_from
|
|
)._get_last_year_average_variable_revenues()
|
|
return fixed_salary + avg_variable_revenues
|
|
|
|
def _get_paid_amount_warrant(self):
|
|
self.ensure_one()
|
|
warrant_input_type = self.env.ref('l10n_be_hr_payroll.cp200_other_input_warrant')
|
|
return sum(self.input_line_ids.filtered(lambda a: a.input_type_id == warrant_input_type).mapped('amount'))
|
|
|
|
def _get_paid_double_holiday(self):
|
|
self.ensure_one()
|
|
contracts = self.employee_id.contract_ids.filtered(lambda c: c.state not in ['draft', 'cancel'] and c.structure_type_id == self.struct_id.type_id)
|
|
if not contracts:
|
|
return 0.0
|
|
|
|
basic = self.contract_id._get_contract_wage()
|
|
force_months = self.input_line_ids.filtered(lambda l: l.code == 'MONTHS')
|
|
|
|
year = self.date_from.year - 1
|
|
date_from = date(year, 1, 1)
|
|
date_to = date(year, 12, 31)
|
|
|
|
if force_months:
|
|
n_months = force_months[0].amount
|
|
fixed_salary = basic * n_months / 12
|
|
else:
|
|
# 1. Number of months
|
|
n_months = self._compute_number_complete_months_of_work(date_from, date_to, contracts)
|
|
# 2. Deduct absences
|
|
presence_prorata = self._compute_presence_prorata(date_from, date_to, contracts)
|
|
fixed_salary = basic * n_months / 12 * presence_prorata
|
|
# 3. Previous Year occupation
|
|
if year == int(self.employee_id.first_contract_year_n1):
|
|
for line in self.employee_id.double_pay_line_n1_ids:
|
|
fixed_salary += basic * line.months_count * line.occupation_rate / 100 / 12
|
|
n_months += line.months_count
|
|
elif year == int(self.employee_id.first_contract_year_n):
|
|
for line in self.employee_id.double_pay_line_n_ids:
|
|
fixed_salary += basic * line.months_count * line.occupation_rate / 100 / 12
|
|
n_months += line.months_count
|
|
|
|
force_avg_variable_revenues = self.input_line_ids.filtered(lambda l: l.code == 'VARIABLE')
|
|
if force_avg_variable_revenues:
|
|
avg_variable_revenues = force_avg_variable_revenues[0].amount
|
|
else:
|
|
if not n_months:
|
|
avg_variable_revenues = 0
|
|
else:
|
|
avg_variable_revenues = self.with_context(
|
|
variable_revenue_date_from=self.date_from
|
|
)._get_last_year_average_variable_revenues()
|
|
return fixed_salary + avg_variable_revenues
|
|
|
|
def _get_paid_amount(self):
|
|
self.ensure_one()
|
|
belgian_payslip = self.struct_id.country_id.code == "BE"
|
|
if belgian_payslip:
|
|
if self.struct_id.code == 'CP200THIRTEEN':
|
|
return self._get_paid_amount_13th_month()
|
|
if self.struct_id.code == 'CP200WARRANT':
|
|
return self._get_paid_amount_warrant()
|
|
if self.struct_id.code == 'CP200DOUBLE':
|
|
return self._get_paid_double_holiday()
|
|
return super()._get_paid_amount()
|
|
|
|
def _is_active_belgian_languages(self):
|
|
active_langs = self.env['res.lang'].with_context(active_test=True).search([]).mapped('code')
|
|
return any(l in active_langs for l in ["fr_BE", "fr_FR", "nl_BE", "nl_NL", "de_BE", "de_DE"])
|
|
|
|
def _get_sum_european_time_off_days(self, check=False):
|
|
self.ensure_one()
|
|
two_years_payslips = self.env['hr.payslip'].search([
|
|
('employee_id', '=', self.employee_id.id),
|
|
('date_to', '<=', date(self.date_from.year, 12, 31)),
|
|
('date_from', '>=', date(self.date_from.year - 2, 1, 1)),
|
|
('state', 'in', ['done', 'paid']),
|
|
])
|
|
european_time_off_amount = two_years_payslips.filtered(lambda p: p.date_from.year < self.date_from.year)._get_worked_days_line_amount('LEAVE216')
|
|
already_recovered_amount = two_years_payslips._get_line_values(['EU.LEAVE.DEDUC'], compute_sum=True)['EU.LEAVE.DEDUC']['sum']['total']
|
|
return european_time_off_amount + already_recovered_amount
|
|
|
|
def _is_invalid(self):
|
|
invalid = super()._is_invalid()
|
|
if not invalid and self._is_active_belgian_languages():
|
|
country = self.struct_id.country_id
|
|
if country.code == 'BE' and self.employee_id.lang not in ["fr_BE", "fr_FR", "nl_BE", "nl_NL", "de_BE", "de_DE"]:
|
|
return _('This document is a translation. This is not a legal document.')
|
|
return invalid
|
|
|
|
def _get_negative_net_input_type(self):
|
|
self.ensure_one()
|
|
if self.struct_id.code == 'CP200MONTHLY':
|
|
return self.env.ref('l10n_be_hr_payroll.input_negative_net')
|
|
return super()._get_negative_net_input_type()
|
|
|
|
def action_payslip_done(self):
|
|
if self._is_active_belgian_languages():
|
|
bad_language_slips = self.filtered(
|
|
lambda p: p.struct_id.country_id.code == "BE" and p.employee_id.lang not in ["fr_BE", "fr_FR", "nl_BE", "nl_NL", "de_BE", "de_DE"])
|
|
if bad_language_slips:
|
|
action = self.env['ir.actions.act_window'].\
|
|
_for_xml_id('l10n_be_hr_payroll.l10n_be_hr_payroll_employee_lang_wizard_action')
|
|
ctx = dict(self.env.context)
|
|
ctx.update({
|
|
'employee_ids': bad_language_slips.employee_id.ids,
|
|
'default_slip_ids': self.ids,
|
|
})
|
|
action['context'] = ctx
|
|
return action
|
|
return super().action_payslip_done()
|
|
|
|
def _get_pdf_reports(self):
|
|
res = super()._get_pdf_reports()
|
|
report_n = self.env.ref('l10n_be_hr_payroll.action_report_termination_holidays_n')
|
|
report_n1 = self.env.ref('l10n_be_hr_payroll.action_report_termination_holidays_n1')
|
|
for payslip in self:
|
|
if payslip.struct_id.code == 'CP200HOLN1':
|
|
res[report_n1] |= payslip
|
|
elif payslip.struct_id.code == 'CP200HOLN':
|
|
res[report_n] |= payslip
|
|
return res
|
|
|
|
def _get_data_files_to_update(self):
|
|
# Note: file order should be maintained
|
|
return super()._get_data_files_to_update() + [(
|
|
'l10n_be_hr_payroll', [
|
|
'data/hr_rule_parameters_data.xml',
|
|
])]
|
|
|
|
def _get_dashboard_warnings(self):
|
|
res = super()._get_dashboard_warnings()
|
|
belgian_companies = self.env.companies.filtered(lambda c: c.country_id.code == 'BE')
|
|
if belgian_companies:
|
|
# NISS VALIDATION
|
|
invalid_niss_employee_ids = self.env['hr.employee']._get_invalid_niss_employee_ids()
|
|
if invalid_niss_employee_ids:
|
|
invalid_niss_str = _('Employees With Invalid NISS Numbers')
|
|
res.append({
|
|
'string': invalid_niss_str,
|
|
'count': len(invalid_niss_employee_ids),
|
|
'action': self._dashboard_default_action(invalid_niss_str, 'hr.employee', invalid_niss_employee_ids),
|
|
})
|
|
|
|
# GENDER VALIDATION
|
|
invalid_gender_employees = self.env['hr.employee'].search([
|
|
('gender', 'not in', ['male', 'female']),
|
|
('company_id', 'in', belgian_companies.ids)
|
|
])
|
|
if invalid_gender_employees:
|
|
invalid_gender_str = _('Employees With Invalid Configured Gender')
|
|
res.append({
|
|
'string': invalid_gender_str,
|
|
'count': len(invalid_gender_employees),
|
|
'action': self._dashboard_default_action(invalid_gender_str, 'hr.employee', invalid_gender_employees.ids),
|
|
})
|
|
|
|
# LANGUAGE VALIDATION
|
|
active_languages = self._is_active_belgian_languages()
|
|
if active_languages:
|
|
invalid_language_employees = self.env['hr.employee'].search([
|
|
('company_id', 'in', belgian_companies.ids)
|
|
]).filtered(lambda e: e.lang not in ["fr_BE", "fr_FR", "nl_BE", "nl_NL", "de_BE", "de_DE"])
|
|
else:
|
|
invalid_language_employees = self.env['hr.employee']
|
|
if invalid_language_employees:
|
|
invalid_gender_str = _('Employees With Invalid Configured Language')
|
|
res.append({
|
|
'string': invalid_gender_str,
|
|
'count': len(invalid_language_employees),
|
|
'action': self._dashboard_default_action(invalid_gender_str, 'hr.employee', invalid_language_employees.ids),
|
|
})
|
|
|
|
# WORK ADDRESS VALIDATION
|
|
address_employees = self.env['hr.employee'].search([
|
|
('company_id', 'in', belgian_companies.ids),
|
|
('employee_type', 'in', ['employee', 'student']),
|
|
('contract_id.state', 'in', ['open', 'close']),
|
|
])
|
|
work_addresses = address_employees.mapped('address_id')
|
|
location_units = self.env['l10n_be.dmfa.location.unit'].search([('partner_id', 'in', work_addresses.ids)])
|
|
invalid_addresses = work_addresses - location_units.mapped('partner_id')
|
|
if invalid_addresses:
|
|
invalid_address_str = _('Work addresses without ONSS identification code')
|
|
res.append({
|
|
'string': invalid_address_str,
|
|
'count': len(invalid_addresses),
|
|
'action': self._dashboard_default_action(invalid_address_str, 'res.partner', invalid_addresses.ids),
|
|
})
|
|
|
|
# SICK MORE THAN 30 DAYS
|
|
sick_work_entry_type = self.env.ref('hr_work_entry_contract.work_entry_type_sick_leave')
|
|
partial_sick_work_entry_type = self.env.ref('l10n_be_hr_payroll.work_entry_type_part_sick')
|
|
long_sick_work_entry_type = self.env.ref('l10n_be_hr_payroll.work_entry_type_long_sick')
|
|
sick_work_entry_types = sick_work_entry_type + partial_sick_work_entry_type + long_sick_work_entry_type
|
|
|
|
sick_more_than_30days_leave = self.env['hr.leave'].search([
|
|
('employee_company_id', '=', self.env.company.id),
|
|
('date_from', '<=', date.today() + relativedelta(days=-31)),
|
|
('holiday_status_id.work_entry_type_id', 'in', sick_work_entry_types.ids),
|
|
('state', '=', 'validate'),
|
|
])
|
|
|
|
employees_on_long_sick_leave = []
|
|
employee_ids = sick_more_than_30days_leave.mapped('employee_id').ids
|
|
for employee_id in employee_ids:
|
|
employee_leaves = sick_more_than_30days_leave.filtered(lambda l: l.employee_id.id == employee_id)
|
|
total_duration = sum([(leave.date_to - leave.date_from).days for leave in employee_leaves])
|
|
if total_duration > 30:
|
|
employees_on_long_sick_leave.append(employee_id)
|
|
|
|
sick_more_than_30days_str = _('Employee on Mutual Health (> 30 days Illness)')
|
|
if employees_on_long_sick_leave:
|
|
res.append({
|
|
'string': sick_more_than_30days_str,
|
|
'count': len(employees_on_long_sick_leave),
|
|
'action': self._dashboard_default_action(sick_more_than_30days_str, 'hr.employee', employees_on_long_sick_leave),
|
|
})
|
|
|
|
return res
|
|
|
|
def _get_ffe_contribution_rate(self, worker_count):
|
|
# Fond de fermeture d'entreprise
|
|
# https://www.socialsecurity.be/employer/instructions/dmfa/fr/latest/instructions/special_contributions/other_specialcontributions/basiccontributions_closingcompanyfunds.html
|
|
self.ensure_one()
|
|
if self.company_id.l10n_be_ffe_employer_type == 'commercial':
|
|
if worker_count < 20:
|
|
rate = self.env['hr.rule.parameter']._get_parameter_from_code('l10n_be_ffe_commercial_rate_low', self.date_to)
|
|
else:
|
|
rate = self.env['hr.rule.parameter']._get_parameter_from_code('l10n_be_ffe_commercial_rate_high', self.date_to)
|
|
else:
|
|
rate = self.env['hr.rule.parameter']._get_parameter_from_code('l10n_be_ffe_noncommercial_rate', self.date_to)
|
|
return rate
|
|
|
|
def _get_be_termination_withholding_rate(self, localdict):
|
|
# See: https://www.securex.eu/lex-go.nsf/vwReferencesByCategory_fr/52DA120D5DCDAE78C12584E000721081?OpenDocument
|
|
self.ensure_one()
|
|
def find_rates(x, rates):
|
|
for low, high, rate in rates:
|
|
if low <= x <= high:
|
|
return rate
|
|
|
|
inputs = localdict['inputs']
|
|
if 'ANNUAL_TAXABLE' not in inputs:
|
|
return 0
|
|
annual_taxable = inputs['ANNUAL_TAXABLE'].amount
|
|
|
|
# Note: Exoneration for children in charge is managed on the salary.rule for the amount
|
|
rates = self._rule_parameter('holiday_pay_pp_rates')
|
|
pp_rate = find_rates(annual_taxable, rates)
|
|
|
|
# Rate Reduction for children in charge
|
|
children = self.employee_id.dependent_children
|
|
children_reduction = self._rule_parameter('holiday_pay_pp_rate_reduction')
|
|
if children and annual_taxable <= children_reduction.get(children, children_reduction[5])[1]:
|
|
pp_rate *= (1 - children_reduction.get(children, children_reduction[5])[0] / 100.0)
|
|
return pp_rate
|
|
|
|
def _get_be_withholding_taxes(self, localdict):
|
|
self.ensure_one()
|
|
|
|
categories = localdict['categories']
|
|
|
|
def compute_basic_bareme(value):
|
|
rates = self._rule_parameter('basic_bareme_rates')
|
|
rates = [(limit or float('inf'), rate) for limit, rate in rates] # float('inf') because limit equals None for last level
|
|
rates = sorted(rates)
|
|
|
|
basic_bareme = 0
|
|
previous_limit = 0
|
|
for limit, rate in rates:
|
|
basic_bareme += max(min(value, limit) - previous_limit, 0) * rate
|
|
previous_limit = limit
|
|
return float_round(basic_bareme, precision_rounding=0.01)
|
|
|
|
def convert_to_month(value):
|
|
return float_round(value / 12.0, precision_rounding=0.01, rounding_method='DOWN')
|
|
|
|
employee = self.contract_id.employee_id
|
|
# PART 1: Withholding tax amount computation
|
|
withholding_tax_amount = 0.0
|
|
|
|
taxable_amount = categories['GROSS'] # Base imposable
|
|
|
|
if self.date_from.year < 2023:
|
|
lower_bound = taxable_amount - taxable_amount % 15
|
|
else:
|
|
lower_bound = taxable_amount
|
|
|
|
# yearly_gross_revenue = Revenu Annuel Brut
|
|
yearly_gross_revenue = lower_bound * 12.0
|
|
|
|
# yearly_net_taxable_amount = Revenu Annuel Net Imposable
|
|
if yearly_gross_revenue <= self._rule_parameter('yearly_gross_revenue_bound_expense'):
|
|
yearly_net_taxable_revenue = yearly_gross_revenue * (1.0 - 0.3)
|
|
else:
|
|
yearly_net_taxable_revenue = yearly_gross_revenue - self._rule_parameter('expense_deduction')
|
|
|
|
# BAREME III: Non resident
|
|
if employee.is_non_resident:
|
|
basic_bareme = compute_basic_bareme(yearly_net_taxable_revenue)
|
|
withholding_tax_amount = convert_to_month(basic_bareme)
|
|
else:
|
|
# BAREME I: Isolated or spouse with income
|
|
if employee.marital in ['divorced', 'single', 'widower'] or (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status != 'without_income'):
|
|
basic_bareme = max(compute_basic_bareme(yearly_net_taxable_revenue) - self._rule_parameter('deduct_single_with_income'), 0.0)
|
|
withholding_tax_amount = convert_to_month(basic_bareme)
|
|
|
|
# BAREME II: spouse without income
|
|
if employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status == 'without_income':
|
|
yearly_net_taxable_revenue_for_spouse = min(yearly_net_taxable_revenue * 0.3, self._rule_parameter('max_spouse_income'))
|
|
basic_bareme_1 = compute_basic_bareme(yearly_net_taxable_revenue_for_spouse)
|
|
basic_bareme_2 = compute_basic_bareme(yearly_net_taxable_revenue - yearly_net_taxable_revenue_for_spouse)
|
|
withholding_tax_amount = convert_to_month(max(basic_bareme_1 + basic_bareme_2 - 2 * self._rule_parameter('deduct_single_with_income'), 0))
|
|
|
|
# Reduction for other family charges
|
|
if (employee.children and employee.dependent_children) or (employee.other_dependent_people and (employee.dependent_seniors or employee.dependent_juniors)):
|
|
if employee.marital in ['divorced', 'single', 'widower'] or (employee.spouse_fiscal_status != 'without_income'):
|
|
|
|
# if employee.marital in ['divorced', 'single', 'widower']:
|
|
# withholding_tax_amount -= self._rule_parameter('isolated_deduction')
|
|
if employee.marital in ['divorced', 'single', 'widower'] and employee.dependent_children:
|
|
withholding_tax_amount -= self._rule_parameter('disabled_dependent_deduction')
|
|
if employee.disabled:
|
|
withholding_tax_amount -= self._rule_parameter('disabled_dependent_deduction')
|
|
if employee.other_dependent_people and employee.dependent_seniors:
|
|
withholding_tax_amount -= self._rule_parameter('dependent_senior_deduction') * employee.dependent_seniors
|
|
if employee.other_dependent_people and employee.dependent_juniors:
|
|
withholding_tax_amount -= self._rule_parameter('disabled_dependent_deduction') * employee.dependent_juniors
|
|
if employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status == 'low_income':
|
|
withholding_tax_amount -= self._rule_parameter('spouse_low_income_deduction')
|
|
if employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status == 'low_pension':
|
|
withholding_tax_amount -= self._rule_parameter('spouse_other_income_deduction')
|
|
if employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status == 'without_income':
|
|
if employee.disabled:
|
|
withholding_tax_amount -= self._rule_parameter('disabled_dependent_deduction')
|
|
if employee.disabled_spouse_bool:
|
|
withholding_tax_amount -= self._rule_parameter('disabled_dependent_deduction')
|
|
if employee.other_dependent_people and employee.dependent_seniors:
|
|
withholding_tax_amount -= self._rule_parameter('dependent_senior_deduction') * employee.dependent_seniors
|
|
if employee.other_dependent_people and employee.dependent_juniors:
|
|
withholding_tax_amount -= self._rule_parameter('disabled_dependent_deduction') * employee.dependent_juniors
|
|
|
|
# Child Allowances
|
|
n_children = employee.dependent_children
|
|
if n_children > 0:
|
|
children_deduction = self._rule_parameter('dependent_basic_children_deduction')
|
|
if n_children <= 8:
|
|
withholding_tax_amount -= children_deduction.get(n_children, 0.0)
|
|
if n_children > 8:
|
|
withholding_tax_amount -= children_deduction.get(8, 0.0) + (n_children - 8) * self._rule_parameter('dependent_children_deduction')
|
|
|
|
if self.contract_id.fiscal_voluntarism:
|
|
voluntary_amount = categories['GROSS'] * self.contract_id.fiscal_voluntary_rate / 100
|
|
if voluntary_amount > withholding_tax_amount:
|
|
withholding_tax_amount = voluntary_amount
|
|
|
|
return - max(withholding_tax_amount, 0.0)
|
|
|
|
def _get_be_special_social_cotisations(self, localdict):
|
|
self.ensure_one()
|
|
|
|
def find_rate(x, rates):
|
|
for low, high, rate, basis, min_amount, max_amount in rates:
|
|
if low <= x <= high:
|
|
return low, high, rate, basis, min_amount, max_amount
|
|
return 0, 0, 0, 0, 0, 0
|
|
|
|
categories = localdict['categories']
|
|
employee = self.contract_id.employee_id
|
|
wage = categories['BASIC']
|
|
if not wage or employee.is_non_resident:
|
|
return 0.0
|
|
|
|
if employee.marital in ['divorced', 'single', 'widower'] or (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status == 'without_income'):
|
|
rates = self._rule_parameter('cp200_monss_isolated')
|
|
if not rates:
|
|
rates = [
|
|
(0.00, 1945.38, 0.00, 0.00, 0.00, 0.00),
|
|
(1945.39, 2190.18, 0.076, 0.00, 0.00, 18.60),
|
|
(2190.19, 6038.82, 0.011, 18.60, 0.00, 60.94),
|
|
(6038.83, 999999999.00, 1.000, 60.94, 0.00, 60.94),
|
|
]
|
|
low, dummy, rate, basis, min_amount, max_amount = find_rate(wage, rates)
|
|
return -min(max(basis + (wage - low + 0.01) * rate, min_amount), max_amount)
|
|
|
|
if employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status != 'without_income':
|
|
rates = self._rule_parameter('cp200_monss_couple')
|
|
if not rates:
|
|
rates = [
|
|
(0.00, 1095.09, 0.00, 0.00, 0.00, 0.00),
|
|
(1095.10, 1945.38, 0.00, 9.30, 9.30, 9.30),
|
|
(1945.39, 2190.18, 0.076, 0.00, 9.30, 18.60),
|
|
(2190.19, 6038.82, 0.011, 18.60, 0.00, 51.64),
|
|
(6038.83, 999999999.00, 1.000, 51.64, 51.64, 51.64),
|
|
]
|
|
low, dummy, rate, basis, min_amount, max_amount = find_rate(wage, rates)
|
|
if isinstance(max_amount, tuple):
|
|
if employee.spouse_fiscal_status in ['high_income', 'low_income']:
|
|
# conjoint avec revenus professionnels
|
|
max_amount = max_amount[0]
|
|
else:
|
|
# conjoint sans revenus professionnels
|
|
max_amount = max_amount[1]
|
|
return -min(max(basis + (wage - low + 0.01) * rate, min_amount), max_amount)
|
|
return 0.0
|
|
|
|
def _get_be_ip(self, localdict):
|
|
self.ensure_one()
|
|
contract = self.contract_id
|
|
if not contract.ip:
|
|
return 0.0
|
|
return self._get_paid_amount() * contract.ip_wage_rate / 100.0
|
|
|
|
def _get_be_ip_deduction(self, localdict):
|
|
self.ensure_one()
|
|
tax_rate = 0.15
|
|
ip_amount = self._get_be_ip(localdict)
|
|
if not ip_amount:
|
|
return 0.0
|
|
ip_deduction_bracket_1 = self._rule_parameter('ip_deduction_bracket_1')
|
|
ip_deduction_bracket_2 = self._rule_parameter('ip_deduction_bracket_2')
|
|
if 0.0 <= ip_amount <= ip_deduction_bracket_1:
|
|
tax_rate = tax_rate / 2.0
|
|
elif ip_deduction_bracket_1 < ip_amount <= ip_deduction_bracket_2:
|
|
tax_rate = tax_rate * 3.0 / 4.0
|
|
return - min(ip_amount * tax_rate, 11745)
|
|
|
|
def _get_employment_bonus_employees_volet_A(self, localdict):
|
|
categories = localdict['categories']
|
|
if not self.worked_days_line_ids and not self.env.context.get('salary_simulation'):
|
|
return 0
|
|
|
|
# S = (W / H) * U
|
|
# W = salaire brut
|
|
# H = le nombre d'heures de travail déclarées avec un code prestations 1, 3, 4, 5 et 20;
|
|
# U = le nombre maximum d'heures de prestations pour le mois concerné dans le régime de travail concerné
|
|
if self.env.context.get('salary_simulation'):
|
|
paid_hours = 1
|
|
total_hours = 1
|
|
else:
|
|
worked_days = self.worked_days_line_ids.filtered(lambda wd: wd.code not in ['LEAVE300', 'LEAVE301', 'MEDIC01'])
|
|
paid_hours = sum(worked_days.filtered(lambda wd: wd.amount).mapped('number_of_hours')) # H
|
|
total_hours = sum(worked_days.mapped('number_of_hours')) # U
|
|
|
|
# 1. - Détermination du salaire mensuel de référence (S)
|
|
salary = categories['BRUT'] * total_hours / paid_hours # S = (W/H) x U
|
|
|
|
# 2. - Détermination du montant de base de la réduction (R)
|
|
bonus_basic_amount_volet_A = self._rule_parameter('work_bonus_basic_amount_volet_A')
|
|
wage_lower_bound = self._rule_parameter('work_bonus_reference_wage_low')
|
|
wage_middle_bound = self._rule_parameter('l10n_be_work_bonus_reference_wage_middle')
|
|
wage_higher_bound = self._rule_parameter('work_bonus_reference_wage_high')
|
|
if salary <= wage_lower_bound:
|
|
result = bonus_basic_amount_volet_A
|
|
elif salary <= wage_middle_bound:
|
|
result = bonus_basic_amount_volet_A
|
|
elif salary <= wage_higher_bound:
|
|
coeff = self._rule_parameter('work_bonus_coeff')
|
|
result = bonus_basic_amount_volet_A - (coeff * (salary - wage_middle_bound))
|
|
else:
|
|
result = 0
|
|
|
|
# 3. - Détermination du montant de la réduction (P)
|
|
result = result * paid_hours / total_hours # P = (H/U) x R
|
|
|
|
return result
|
|
|
|
def _get_employment_bonus_employees_volet_B(self, localdict):
|
|
categories = localdict['categories']
|
|
if not self.worked_days_line_ids and not self.env.context.get('salary_simulation'):
|
|
return 0
|
|
|
|
# S = (W / H) * U
|
|
# W = salaire brut
|
|
# H = le nombre d'heures de travail déclarées avec un code prestations 1, 3, 4, 5 et 20;
|
|
# U = le nombre maximum d'heures de prestations pour le mois concerné dans le régime de travail concerné
|
|
if self.env.context.get('salary_simulation'):
|
|
paid_hours = 1
|
|
total_hours = 1
|
|
else:
|
|
worked_days = self.worked_days_line_ids.filtered(lambda wd: wd.code not in ['LEAVE300', 'LEAVE301', 'MEDIC01'])
|
|
paid_hours = sum(worked_days.filtered(lambda wd: wd.amount).mapped('number_of_hours')) # H
|
|
total_hours = sum(worked_days.mapped('number_of_hours')) # U
|
|
|
|
# 1. - Détermination du salaire mensuel de référence (S)
|
|
salary = categories['BRUT'] * total_hours / paid_hours # S = (W/H) x U
|
|
|
|
# 2. - Détermination du montant de base de la réduction (R)
|
|
bonus_basic_amount = self._rule_parameter('work_bonus_basic_amount')
|
|
wage_lower_bound = self._rule_parameter('work_bonus_reference_wage_low')
|
|
wage_middle_bound = self._rule_parameter('l10n_be_work_bonus_reference_wage_middle')
|
|
wage_higher_bound = self._rule_parameter('work_bonus_reference_wage_high')
|
|
if salary <= wage_lower_bound:
|
|
result = bonus_basic_amount
|
|
elif salary <= wage_middle_bound:
|
|
coeff = self._rule_parameter('l10n_be_work_bonus_coeff_low')
|
|
result = bonus_basic_amount - (coeff * (salary - wage_lower_bound))
|
|
elif salary <= wage_higher_bound:
|
|
result = 0
|
|
else:
|
|
result = 0
|
|
|
|
# 3. - Détermination du montant de la réduction (P)
|
|
result = result * paid_hours / total_hours # P = (H/U) x R
|
|
|
|
return result
|
|
|
|
# ref: https://www.socialsecurity.be/employer/instructions/dmfa/fr/latest/instructions/deductions/workers_reductions/workbonus.html
|
|
def _get_employment_bonus_employees(self, localdict):
|
|
self.ensure_one()
|
|
categories = localdict['categories']
|
|
if self.date_from >= date(2024, 4, 1):
|
|
bonus_volet_A = self._get_employment_bonus_employees_volet_A(localdict)
|
|
bonus_volet_B = self._get_employment_bonus_employees_volet_B(localdict)
|
|
result = bonus_volet_A + bonus_volet_B
|
|
# Nasty lazy dev
|
|
localdict['result_rules']['bonus_volet_A']['total'] = bonus_volet_A
|
|
localdict['result_rules']['bonus_volet_B']['total'] = bonus_volet_B
|
|
return min(result, -categories['ONSS'])
|
|
|
|
bonus_basic_amount = self._rule_parameter('work_bonus_basic_amount')
|
|
wage_lower_bound = self._rule_parameter('work_bonus_reference_wage_low')
|
|
if not self.worked_days_line_ids and not self.env.context.get('salary_simulation'):
|
|
return 0
|
|
|
|
# S = (W / H) * U
|
|
# W = salaire brut
|
|
# H = le nombre d'heures de travail déclarées avec un code prestations 1, 3, 4, 5 et 20;
|
|
# U = le nombre maximum d'heures de prestations pour le mois concerné dans le régime de travail concerné
|
|
if self.env.context.get('salary_simulation'):
|
|
paid_hours = 1
|
|
total_hours = 1
|
|
else:
|
|
worked_days = self.worked_days_line_ids.filtered(lambda wd: wd.code not in ['LEAVE300', 'LEAVE301', 'MEDIC01'])
|
|
paid_hours = sum(worked_days.filtered(lambda wd: wd.amount).mapped('number_of_hours')) # H
|
|
total_hours = sum(worked_days.mapped('number_of_hours')) # U
|
|
|
|
# 1. - Détermination du salaire mensuel de référence (S)
|
|
salary = categories['BRUT'] * total_hours / paid_hours # S = (W/H) x U
|
|
|
|
# 2. - Détermination du montant de base de la réduction (R)
|
|
if self.date_from < date(2023, 7, 1):
|
|
if salary <= wage_lower_bound:
|
|
result = bonus_basic_amount
|
|
elif salary <= self._rule_parameter('work_bonus_reference_wage_high'):
|
|
coeff = self._rule_parameter('work_bonus_coeff')
|
|
result = bonus_basic_amount - (coeff * (salary - wage_lower_bound))
|
|
else:
|
|
result = 0
|
|
else:
|
|
if salary <= wage_lower_bound:
|
|
result = bonus_basic_amount
|
|
elif salary <= self._rule_parameter('l10n_be_work_bonus_reference_wage_middle'):
|
|
coeff = self._rule_parameter('l10n_be_work_bonus_coeff_low')
|
|
result = bonus_basic_amount - (coeff * (salary - wage_lower_bound))
|
|
elif salary <= self._rule_parameter('work_bonus_reference_wage_high'):
|
|
coeff = self._rule_parameter('work_bonus_coeff')
|
|
result = bonus_basic_amount - (coeff * (salary - wage_lower_bound))
|
|
else:
|
|
result = 0
|
|
|
|
# 3. - Détermination du montant de la réduction (P)
|
|
result = result * paid_hours / total_hours # P = (H/U) x R
|
|
|
|
return min(result, -categories['ONSS'])
|
|
|
|
def _get_withholding_taxes_after_child_allowances(self, rates, gross, apply_reduction=True):
|
|
|
|
def find_rates(x, rates):
|
|
for low, high, rate in rates:
|
|
if low <= x <= high:
|
|
return rate / 100.0
|
|
|
|
children_exoneration = self._rule_parameter('holiday_pay_pp_exoneration')
|
|
children_reduction = self._rule_parameter('holiday_pay_pp_rate_reduction')
|
|
|
|
employee = self.contract_id.employee_id
|
|
|
|
contract = self.contract_id
|
|
monthly_revenue = contract._get_contract_wage()
|
|
# Count ANT in yearly remuneration
|
|
if contract.internet:
|
|
monthly_revenue += 5.0
|
|
if contract.mobile and not contract.internet:
|
|
monthly_revenue += 4.0 + 5.0
|
|
if contract.mobile and contract.internet:
|
|
monthly_revenue += 4.0
|
|
if contract.has_laptop:
|
|
monthly_revenue += 7.0
|
|
|
|
yearly_revenue = monthly_revenue * (1 - 0.1307) * 12.0
|
|
|
|
if contract.transport_mode_car:
|
|
if 'vehicle_id' in self:
|
|
yearly_revenue += self.vehicle_id._get_car_atn(date=self.date_from) * 12.0
|
|
else:
|
|
yearly_revenue += contract.car_atn * 12.0
|
|
|
|
# Exoneration
|
|
children = employee.dependent_children
|
|
if children > 0 and yearly_revenue <= children_exoneration.get(children, children_exoneration[12]):
|
|
yearly_revenue -= children_exoneration.get(children, children_exoneration[12]) - yearly_revenue
|
|
yearly_revenue = max(yearly_revenue, 0)
|
|
|
|
withholding_tax_amount = gross * find_rates(yearly_revenue, rates)
|
|
# Reduction
|
|
if (apply_reduction and
|
|
children > 0 and
|
|
yearly_revenue <= children_reduction.get(children, children_reduction[5])[1]
|
|
):
|
|
withholding_tax_amount *= (1 - children_reduction.get(children, children_reduction[5])[0] / 100.0)
|
|
|
|
return - withholding_tax_amount
|
|
|
|
def _get_be_double_holiday_withholding_taxes(self, localdict):
|
|
self.ensure_one()
|
|
# See: https://www.securex.eu/lex-go.nsf/vwReferencesByCategory_fr/52DA120D5DCDAE78C12584E000721081?OpenDocument
|
|
|
|
rates = self._rule_parameter('holiday_pay_pp_rates')
|
|
|
|
categories = localdict['categories']
|
|
if self.struct_id.code == "CP200DOUBLE":
|
|
gross = categories['GROSS']
|
|
elif self.struct_id.code == "CP200MONTHLY":
|
|
gross = categories['DDPG']
|
|
|
|
return self._get_withholding_taxes_after_child_allowances(rates, gross)
|
|
|
|
def _get_thirteen_month_withholding_taxes(self, localdict):
|
|
self.ensure_one()
|
|
# See: https://www.securex.eu/lex-go.nsf/vwReferencesByCategory_fr/52DA120D5DCDAE78C12584E000721081?OpenDocument
|
|
|
|
rates = self._rule_parameter('exceptional_allowances_pp_rates')
|
|
categories = localdict['categories']
|
|
gross = categories['GROSS']
|
|
|
|
return self._get_withholding_taxes_after_child_allowances(rates, gross)
|
|
|
|
def _get_termination_fees_withholding_taxes(self, localdict):
|
|
# See: https://www.securex.eu/lex-go.nsf/vwReferencesByCategory_fr/52DA120D5DCDAE78C12584E000721081?OpenDocument
|
|
if self.date_from.year >= 2024:
|
|
self.ensure_one()
|
|
|
|
rates = self._rule_parameter('termination_fees_pp_rates')
|
|
categories = localdict['categories']
|
|
gross = categories['GROSS']
|
|
|
|
return self._get_withholding_taxes_after_child_allowances(rates, gross, apply_reduction=False)
|
|
|
|
else:
|
|
return self._get_be_withholding_taxes(localdict)
|
|
|
|
def _get_withholding_reduction(self, localdict):
|
|
self.ensure_one()
|
|
categories = localdict['categories']
|
|
if categories['EmpBonus']:
|
|
if self.date_from >= date(2024, 4, 1):
|
|
bonus_volet_A = localdict['result_rules']['bonus_volet_A']['total']
|
|
bonus_volet_B = localdict['result_rules']['bonus_volet_B']['total']
|
|
reduction = bonus_volet_A * 0.3314 + bonus_volet_B * 0.5254
|
|
else:
|
|
reduction = categories['EmpBonus'] * 0.3314
|
|
return min(abs(categories['PP']), reduction)
|
|
return 0.0
|
|
|
|
def _get_impulsion_plan_amount(self, localdict):
|
|
self.ensure_one()
|
|
start = self.employee_id.first_contract_date
|
|
end = self.date_to
|
|
number_of_months = (end.year - start.year) * 12 + (end.month - start.month)
|
|
numerator = sum(wd.number_of_hours for wd in self.worked_days_line_ids if wd.amount > 0)
|
|
denominator = 4 * self.contract_id.resource_calendar_id.hours_per_week
|
|
coefficient = numerator / denominator
|
|
if self.contract_id.l10n_be_impulsion_plan == '25yo':
|
|
if 0 <= number_of_months <= 23:
|
|
theorical_amount = 500.0
|
|
elif 24 <= number_of_months <= 29:
|
|
theorical_amount = 250.0
|
|
elif 30 <= number_of_months <= 35:
|
|
theorical_amount = 125.0
|
|
else:
|
|
theorical_amount = 0
|
|
return min(theorical_amount, theorical_amount * coefficient)
|
|
if self.contract_id.l10n_be_impulsion_plan == '12mo':
|
|
if 0 <= number_of_months <= 11:
|
|
theorical_amount = 500.0
|
|
elif 12 <= number_of_months <= 17:
|
|
theorical_amount = 250.0
|
|
elif 18 <= number_of_months <= 23:
|
|
theorical_amount = 125.0
|
|
else:
|
|
theorical_amount = 0
|
|
return min(theorical_amount, theorical_amount * coefficient)
|
|
return 0
|
|
|
|
def _get_onss_restructuring(self, localdict):
|
|
self.ensure_one()
|
|
# Source: https://www.onem.be/fr/documentation/feuille-info/t115
|
|
|
|
# 1. Grant condition
|
|
# A worker who has been made redundant following a restructuring benefits from a reduction in his personal contributions under certain conditions:
|
|
# - The engagement must take place during the validity period of the reduction card. The reduction card is valid for 6 months, calculated from date to date, following the termination of the employment contract.
|
|
# - The gross monthly reference salary does not exceed
|
|
# o 3.071.90: if the worker is under 30 years of age at the time of entry into service
|
|
# o 4,504.93: if the worker is at least 30 years old at the time of entry into service
|
|
# 2. Amount of reduction
|
|
# Lump sum reduction of € 133.33 per month (full time - full month) in personal social security contributions.
|
|
# If the worker does not work full time for a full month or if he works part time, this amount is reduced proportionally.
|
|
|
|
# So the reduction is:
|
|
# 1. Full-time worker: P = (J / D) x 133.33
|
|
# - Full time with full one month benefits: € 133.33
|
|
|
|
# Example the worker entered service on 02/01/2021 and worked the whole month
|
|
# - Full time with incomplete services: P = (J / D) x 133.33
|
|
# Example: the worker entered service on February 15 -> (10/20) x 133.33 = € 66.665
|
|
# P = amount of reduction
|
|
# J = the number of worker's days declared with a benefit code 1, 3, 4, 5 and 20 .;
|
|
# D = the maximum number of days of benefits for the month concerned in the work scheme concerned.
|
|
|
|
# 2. Part-time worker: P = (H / U) x 133.33
|
|
# Example: the worker starts 02/01/2021 and works 19 hours a week.
|
|
# (76/152) x 133.33 = € 66.665
|
|
# Example: the worker starts 02/15/2021 and works 19 hours a week.
|
|
# (38/155) x 133.33 = 33.335 €
|
|
|
|
# P = amount of reduction
|
|
# H = the number of working hours declared with a service code 1, 3, 4, 5 and 20;
|
|
# U = the number of monthly hours corresponding to D.
|
|
|
|
# 3. Duration of this reduction
|
|
# The benefit applies to all periods of occupation that fall within the period that:
|
|
# starts to run on the day you start your first occupation during the validity period of the restructuring reduction card;
|
|
# and which ends on the last day of the second quarter following the start date of this first occupation.
|
|
# 4. Formalities to be completed
|
|
# The employer deducts the lump sum from the normal amount of personal contributions when paying the remuneration.
|
|
# The ONEM communicates to the ONSS the data concerning the identification of the worker and the validity date of the card.
|
|
|
|
# 5. Point of attention
|
|
# If the worker also benefits from a reduction in his personal contributions for low wages, the cumulation between this reduction and that for restructuring cannot exceed the total amount of personal contributions due.
|
|
|
|
# If this is the case, we must first reduce the restructuring reduction.
|
|
|
|
# Example:
|
|
# - personal contributions = 200 €
|
|
# - restructuring reduction = € 133.33
|
|
# - low salary reduction = 100 €
|
|
|
|
# The total amount of reductions exceeds the contributions due. We must therefore first reduce the restructuring reduction and then the balance of the low wage reduction.
|
|
if not self.worked_days_line_ids:
|
|
return 0
|
|
|
|
employee = self.contract_id.employee_id
|
|
first_contract_date = employee.first_contract_date
|
|
birthdate = employee.birthday
|
|
age = relativedelta(first_contract_date, birthdate).years
|
|
if age < 30:
|
|
threshold = self._rule_parameter('onss_restructuring_before_30')
|
|
else:
|
|
threshold = self._rule_parameter('onss_restructuring_after_30')
|
|
|
|
salary = self.paid_amount
|
|
if salary > threshold:
|
|
return 0
|
|
|
|
amount = self._rule_parameter('onss_restructuring_amount')
|
|
|
|
paid_hours = sum(self.worked_days_line_ids.filtered(lambda wd: wd.amount).mapped('number_of_hours'))
|
|
total_hours = sum(self.worked_days_line_ids.mapped('number_of_hours'))
|
|
ratio = paid_hours / total_hours if total_hours else 0
|
|
|
|
start = first_contract_date
|
|
end = self.date_to
|
|
number_of_months = (end.year - start.year) * 12 + (end.month - start.month)
|
|
if 0 <= number_of_months <= 6:
|
|
return amount * ratio
|
|
return 0
|
|
|
|
def _get_representation_fees_threshold(self, localdict):
|
|
return self._rule_parameter('cp200_representation_fees_threshold')
|
|
|
|
def _get_representation_fees(self, localdict):
|
|
self.ensure_one()
|
|
categories = localdict['categories']
|
|
worked_days = localdict['worked_days']
|
|
# Representation fees aren't paid if there's no basic pay or no time worked
|
|
if categories['BASIC'] and (
|
|
not all(day.work_entry_type_id.is_leave for day in worked_days.values())
|
|
or self.env.context.get('salary_simulation')
|
|
):
|
|
contract = self.contract_id
|
|
calendar = contract.resource_calendar_id
|
|
days_per_week = calendar._get_days_per_week()
|
|
incapacity_attendances = calendar.attendance_ids.filtered(lambda a: a.work_entry_type_id.code == 'LEAVE281')
|
|
if incapacity_attendances:
|
|
incapacity_hours = sum((attendance.hour_to - attendance.hour_from) for attendance in incapacity_attendances)
|
|
incapacity_hours = incapacity_hours / 2 if calendar.two_weeks_calendar else incapacity_hours
|
|
incapacity_rate = (1 - incapacity_hours / calendar.hours_per_week) if calendar.hours_per_week else 0
|
|
work_time_rate = contract.resource_calendar_id.work_time_rate * incapacity_rate
|
|
else:
|
|
work_time_rate = contract.resource_calendar_id.work_time_rate
|
|
|
|
threshold = 0 if ('OUT' in worked_days and worked_days['OUT'].number_of_hours) else self._get_representation_fees_threshold(localdict)
|
|
if days_per_week and self.env.context.get('salary_simulation_full_time'):
|
|
result = contract.representation_fees
|
|
elif days_per_week and contract.representation_fees > threshold:
|
|
# Only part of the representation costs are pro-rated because certain costs are fully
|
|
# covered for the company (teleworking costs, mobile phone, internet, etc., namely (for 2021):
|
|
# - 144.31 € (Tax, since 2021 - coronavirus)
|
|
# - 30 € (internet)
|
|
# - 25 € (phone)
|
|
# - 80 € (car management fees)
|
|
# = Total € 279.31
|
|
# Legally, they are not prorated according to the occupancy fraction.
|
|
# In summary, those who select amounts of for example 150 € and 250 €, have nothing pro-rated
|
|
# because the amounts are covered in an irreducible way.
|
|
# For those who have selected the maximum of 399 €, there is therefore only the share of
|
|
# +-120 € of representation expenses which is then subject to prorating.
|
|
|
|
# Credit time, but with only half days (otherwise it's taken into account)
|
|
if contract.time_credit and work_time_rate and work_time_rate < 100 and (days_per_week == 5 or not self.representation_fees_missing_days):
|
|
total_amount = threshold + (contract.representation_fees - threshold) * work_time_rate / 100
|
|
# Contractual part time
|
|
elif not contract.time_credit and work_time_rate < 100:
|
|
total_amount = threshold + (contract.representation_fees - threshold) * work_time_rate / 100
|
|
else:
|
|
total_amount = contract.representation_fees
|
|
|
|
if total_amount > threshold:
|
|
daily_amount = (total_amount - threshold) * 3 / 13 / days_per_week
|
|
result = max(0, total_amount - daily_amount * self.representation_fees_missing_days)
|
|
elif days_per_week:
|
|
result = contract.representation_fees
|
|
else:
|
|
result = 0
|
|
else:
|
|
result = 0
|
|
return float_round(result, precision_digits=2)
|
|
|
|
def _get_serious_representation_fees(self, localdict):
|
|
self.ensure_one()
|
|
return min(self._get_representation_fees(localdict), self._get_representation_fees_threshold(localdict))
|
|
|
|
def _get_volatile_representation_fees(self, localdict):
|
|
self.ensure_one()
|
|
return max(self._get_representation_fees(localdict) - self._get_representation_fees_threshold(localdict), 0)
|
|
|
|
def _get_holiday_pay_recovery(self, localdict, recovery_type):
|
|
"""
|
|
See: https://www.socialsecurity.be/employer/instructions/dmfa/fr/latest/intermediates#intermediate_row_196b32c7-9d98-4233-805d-ca9bf123ff48
|
|
|
|
When an employee changes employer, he receives the termination pay and a vacation certificate
|
|
stating his vacation rights. When he subsequently takes vacation with his new employer, the latter
|
|
must, when paying the simple vacation pay, take into account the termination pay that the former
|
|
employer has already paid.
|
|
|
|
From an exchange of letters with the SPF ETCS and the Inspectorate responsible for the control of
|
|
social laws, it turned out that when calculating the simple vacation pay, the new employer must
|
|
deduct the exit pay based on the number of vacation days taken. The rule in the ONSS instructions
|
|
according to which the new employer must take into account the exit vacation pay only once when the
|
|
employee takes his main vacation is abolished.
|
|
|
|
When the salary of an employee with his new employer is higher than the salary he had with his
|
|
previous employer, his new employer will have, each time he takes vacation days, to make a
|
|
calculation to supplement the nest egg. exit from these days up to the amount of the simple vacation
|
|
pay to which the worker is entitled.
|
|
|
|
Concretely:
|
|
|
|
2020 vacation certificate (full year):
|
|
- simple allowance 1,917.50 EUR
|
|
- this amounts to 1917.50 / 20 EUR = 95.875 EUR per day of vacation
|
|
- holidays 2021, for example when taking 5 days in April 2021
|
|
- monthly salary with the new employer: 3000.00 EUR / month
|
|
- simple nest egg:
|
|
- remuneration code 12: 5/20 x 1917.50 = 479.38 EUR
|
|
- remuneration code 1: (5/22 x 3000.00) - 479.38 = 202.44 EUR
|
|
- ordinary days for the month of April:
|
|
- remuneration code 1: 17/22 x 3000.00 = 2318.18 EUR
|
|
- The examples included in the ONSS instructions will be adapted in the next publication.
|
|
"""
|
|
self.ensure_one()
|
|
worked_days = localdict['worked_days']
|
|
if 'LEAVE120' not in worked_days or not worked_days['LEAVE120'].amount:
|
|
return 0
|
|
employee = self.employee_id
|
|
number_of_days = employee['l10n_be_holiday_pay_number_of_days_' + recovery_type]
|
|
all_payslips_during_civil_year = self.env['hr.payslip'].search([
|
|
('employee_id', '=', employee.id),
|
|
('date_from', '>=', date(self.date_from.year, 1, 1)),
|
|
('date_to', '<=', date(self.date_from.year, 12, 31)),
|
|
('state', 'in', ['done', 'paid']),
|
|
])
|
|
remaining_day = number_of_days - all_payslips_during_civil_year._get_worked_days_line_number_of_days('LEAVE120')
|
|
if remaining_day <= 0:
|
|
return 0
|
|
if self.wage_type == 'hourly':
|
|
employee_hourly_cost = self.contract_id.hourly_wage
|
|
else:
|
|
if self.date_from.year < 2024:
|
|
employee_hourly_cost = self.contract_id.contract_wage / self.sum_worked_hours
|
|
else:
|
|
employee_hourly_cost = self.contract_id.contract_wage * 3 / 13 / self.contract_id.resource_calendar_id.hours_per_week
|
|
remaining_day_amount = min(remaining_day, number_of_days) * employee_hourly_cost * 7.6
|
|
days_to_recover = employee['l10n_be_holiday_pay_to_recover_' + recovery_type]
|
|
max_amount_to_recover = min(days_to_recover, employee_hourly_cost * number_of_days * 7.6)
|
|
leave120_amount = self._get_worked_days_line_amount('LEAVE120')
|
|
holiday_amount = min(leave120_amount, employee_hourly_cost * self._get_worked_days_line_number_of_hours('LEAVE120'))
|
|
remaining_amount = max(0, max_amount_to_recover - employee['l10n_be_holiday_pay_recovered_' + recovery_type])
|
|
return - min(remaining_amount, remaining_day_amount, holiday_amount)
|
|
|
|
def _get_holiday_pay_recovery_n(self, localdict):
|
|
return self._get_holiday_pay_recovery(localdict, 'n')
|
|
|
|
def _get_holiday_pay_recovery_n1(self, localdict):
|
|
return self._get_holiday_pay_recovery(localdict, 'n1')
|
|
|
|
def _get_termination_n_basic_double(self, localdict):
|
|
self.ensure_one()
|
|
inputs = localdict['inputs']
|
|
result_qty = 1
|
|
result_rate = 6.8
|
|
result = inputs['GROSS_REF'].amount if 'GROSS_REF' in inputs else 0
|
|
date_from = self.date_from
|
|
if self.struct_id.code == "CP200HOLN1":
|
|
existing_double_pay = self.env['hr.payslip'].search([
|
|
('employee_id', '=', self.employee_id.id),
|
|
('state', 'in', ['done', 'paid']),
|
|
('struct_id', '=', self.env.ref('l10n_be_hr_payroll.hr_payroll_structure_cp200_double_holiday').id),
|
|
('date_from', '>=', date(date_from.year, 1, 1)),
|
|
('date_to', '<=', date(date_from.year, 12, 31)),
|
|
])
|
|
if existing_double_pay:
|
|
result = 0
|
|
return (result_qty, result_rate, result)
|