1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_be_hr_payroll/models/hr_contract.py
2024-12-10 09:04:09 +07:00

799 lines
43 KiB
Python

# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from collections import defaultdict
from datetime import datetime
from dateutil.relativedelta import relativedelta
from dateutil.rrule import rrule, DAILY
from odoo import api, fields, models, _
from odoo.tools import float_round, date_utils
from odoo.tools.float_utils import float_compare
from odoo.exceptions import ValidationError
EMPLOYER_ONSS = 0.2714
class HrContract(models.Model):
_inherit = 'hr.contract'
transport_mode_car = fields.Boolean('Uses company car')
transport_mode_private_car = fields.Boolean('Uses private car')
transport_mode_train = fields.Boolean('Uses train transportation')
transport_mode_public = fields.Boolean('Uses another public transportation')
car_atn = fields.Monetary(string='Car BIK', help='Benefit in Kind (Company Car)')
train_transport_employee_amount = fields.Monetary('Train transport paid by the employee (Monthly)')
public_transport_employee_amount = fields.Monetary('Public transport paid by the employee (Monthly)')
warrant_value_employee = fields.Monetary(compute='_compute_commission_cost', string="Warrant monthly value for the employee")
meal_voucher_paid_by_employer = fields.Monetary(compute='_compute_meal_voucher_info', string="Meal Voucher Paid by Employer")
meal_voucher_paid_monthly_by_employer = fields.Monetary(compute='_compute_meal_voucher_info')
company_car_total_depreciated_cost = fields.Monetary()
private_car_reimbursed_amount = fields.Monetary(compute='_compute_private_car_reimbursed_amount')
km_home_work = fields.Integer(related="employee_id.km_home_work", related_sudo=True, readonly=False)
train_transport_reimbursed_amount = fields.Monetary(
string='Train Transport Reimbursed amount',
compute='_compute_train_transport_reimbursed_amount', readonly=False, store=True)
public_transport_reimbursed_amount = fields.Monetary(
string='Public Transport Reimbursed amount',
compute='_compute_public_transport_reimbursed_amount', readonly=False, store=True)
warrants_cost = fields.Monetary(compute='_compute_commission_cost', string="Warrant monthly cost for the employer")
yearly_commission = fields.Monetary(compute='_compute_commission_cost')
yearly_commission_cost = fields.Monetary(compute='_compute_commission_cost')
# Advantages
commission_on_target = fields.Monetary(
string="Commission",
tracking=True,
help="Monthly gross amount that the employee receives if the target is reached.")
fuel_card = fields.Monetary(
string="Fuel Card",
tracking=True,
help="Monthly amount the employee receives on his fuel card.")
internet = fields.Monetary(
string="Internet Subscription",
tracking=True,
help="The employee's internet subcription will be paid up to this amount.")
representation_fees = fields.Monetary(
string="Expense Fees",
tracking=True,
help="Monthly net amount the employee receives to cover his representation fees.")
mobile = fields.Monetary(
string="Mobile Subscription",
tracking=True,
help="The employee's mobile subscription will be paid up to this amount.")
has_laptop = fields.Boolean(
string="Laptop",
tracking=True,
help="A benefit in kind is paid when the employee uses its laptop at home.")
has_bicycle = fields.Boolean(string="Bicycle to work", default=False, groups="hr_contract.group_hr_contract_manager",
help="Use a bicycle as a transport mode to go to work")
meal_voucher_amount = fields.Monetary(
string="Meal Vouchers",
tracking=True,
help="Amount the employee receives in the form of meal vouchers per worked day.")
meal_voucher_average_monthly_amount = fields.Monetary(compute="_compute_meal_voucher_info")
eco_checks = fields.Monetary(
"Eco Vouchers",
help="Yearly amount the employee receives in the form of eco vouchers.")
ip = fields.Boolean('Intellectual Property', default=False, tracking=True)
ip_wage_rate = fields.Float(string="IP percentage", help="Should be between 0 and 100 %")
ip_value = fields.Float(compute='_compute_ip_value')
fiscal_voluntarism = fields.Boolean(
string="Fiscal Voluntarism", default=False, tracking=True,
help="Voluntarily increase withholding tax rate.")
fiscal_voluntary_rate = fields.Float(string="Fiscal Voluntary Rate", help="Should be between 0 and 100 %")
no_onss = fields.Boolean(string="No ONSS")
no_withholding_taxes = fields.Boolean()
rd_percentage = fields.Integer("Time Percentage in R&D")
employee_age = fields.Integer('Age of Employee', compute='_compute_employee_age', compute_sudo=True)
l10n_be_impulsion_plan = fields.Selection([
('25yo', '< 25 years old'),
('12mo', '12 months +'),
('55yo', '55+ years old')], string="Impulsion Plan")
l10n_be_onss_restructuring = fields.Boolean(string="Allow ONSS Reduction for Restructuring")
has_hospital_insurance = fields.Boolean(string="Has Hospital Insurance", groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
insured_relative_children = fields.Integer(string="# Insured Children < 19 y/o", groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
insured_relative_adults = fields.Integer(string="# Insured Children >= 19 y/o", groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
insured_relative_spouse = fields.Boolean(string="Insured Spouse", groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
hospital_insurance_amount_per_child = fields.Float(string="Amount per Child", groups="hr_contract.group_hr_contract_employee_manager",
default=lambda self: float(self.env['ir.config_parameter'].sudo().get_param('hr_contract_salary.hospital_insurance_amount_child', default=7.2)))
hospital_insurance_amount_per_adult = fields.Float(string="Amount per Adult", groups="hr_contract.group_hr_contract_employee_manager",
default=lambda self: float(self.env['ir.config_parameter'].sudo().get_param('hr_contract_salary.hospital_insurance_amount_adult', default=20.5)))
insurance_amount = fields.Float(compute='_compute_insurance_amount', string="Insurance Amount", groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
insured_relative_adults_total = fields.Integer(compute='_compute_insured_relative_adults_total', groups="hr_contract.group_hr_contract_employee_manager")
l10n_be_hospital_insurance_notes = fields.Text(string="Hospital Insurance: Additional Info")
wage_with_holidays = fields.Monetary(
string="Wage With Sacrifices",
help="Adapted salary, according to the sacrifices defined on the contract (Example: Extra-legal time off, a percentage of the salary invested in a group insurance, etc...)")
# Group Insurance
l10n_be_group_insurance_rate = fields.Float(
string="Group Insurance Sacrifice Rate", tracking=True,
help="Should be between 0 and 100 %")
l10n_be_group_insurance_amount = fields.Monetary(
compute='_compute_l10n_be_group_insurance_amount', store=True)
l10n_be_group_insurance_cost = fields.Monetary(
compute='_compute_l10n_be_group_insurance_amount', store=True)
# Ambulatory Insurance
l10n_be_has_ambulatory_insurance = fields.Boolean(
string="Has Ambulatory Insurance",
groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
l10n_be_ambulatory_insured_children = fields.Integer(
string="Ambulatory: # Insured Children < 19 y/o",
groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
l10n_be_ambulatory_insured_adults = fields.Integer(
string="Ambulatory: # Insured Children >= 19 y/o",
groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
l10n_be_ambulatory_insured_spouse = fields.Boolean(
string="Ambulatory: Insured Spouse",
groups="hr_contract.group_hr_contract_employee_manager", tracking=True)
l10n_be_ambulatory_amount_per_child = fields.Float(
string="Ambulatory: Amount per Child", groups="hr_contract.group_hr_contract_employee_manager",
default=lambda self: float(self.env['ir.config_parameter'].sudo().get_param('hr_contract_salary.ambulatory_insurance_amount_child', default=7.2)))
l10n_be_ambulatory_amount_per_adult = fields.Float(
string="Ambulatory: Amount per Adult", groups="hr_contract.group_hr_contract_employee_manager",
default=lambda self: float(self.env['ir.config_parameter'].sudo().get_param('hr_contract_salary.ambulatory_insurance_amount_adult', default=20.5)))
l10n_be_ambulatory_insurance_amount = fields.Float(
compute='_compute_ambulatory_insurance_amount', string="Ambulatory: Insurance Amount",
groups="hr_contract.group_hr_contract_employee_manager", compute_sudo=True, tracking=True)
l10n_be_ambulatory_insured_adults_total = fields.Integer(
compute='_compute_ambulatory_insured_adults_total',
groups="hr_contract.group_hr_contract_employee_manager")
l10n_be_ambulatory_insurance_notes = fields.Text(string="Ambulatory Insurance: Additional Info")
l10n_be_is_below_scale = fields.Boolean(
string="Is below CP200 salary scale", compute='_compute_l10n_be_is_below_scale', search='_search_l10n_be_is_below_scale', compute_sudo=True)
l10n_be_is_below_scale_warning = fields.Char(compute='_compute_l10n_be_is_below_scale', compute_sudo=True)
l10n_be_canteen_cost = fields.Monetary(string="Canteen Cost")
_sql_constraints = [
('check_percentage_ip_rate', 'CHECK(ip_wage_rate >= 0 AND ip_wage_rate <= 100)', 'The IP rate on wage should be between 0 and 100.'),
('check_percentage_fiscal_voluntary_rate', 'CHECK(fiscal_voluntary_rate >= 0 AND fiscal_voluntary_rate <= 100)', 'The Fiscal Voluntary rate on wage should be between 0 and 100.'),
('check_percentage_group_insurance_rate', 'CHECK(l10n_be_group_insurance_rate >= 0 AND l10n_be_group_insurance_rate <= 100)', 'The group insurance salary sacrifice rate on wage should be between 0 and 100.'),
]
@api.depends(
'wage', 'state', 'employee_id.l10n_be_scale_seniority', 'job_id.l10n_be_scale_category',
'work_time_rate', 'time_credit', 'resource_calendar_id.work_time_rate')
def _compute_l10n_be_is_below_scale(self):
# Source: https://emploi.belgique.be/fr/themes/remuneration/salaires-minimums-par-sous-commission-paritaire/banque-de-donnees-salaires
student_stucture_type = self.env.ref('hr_contract.structure_type_employee_cp200')
open_contracts = self.filtered(
lambda c: c.state in ['draft', 'open']
and c.company_id.country_id.code == 'BE'
and c.employee_id
and c._get_contract_wage()
and c.structure_type_id == student_stucture_type)
(self - open_contracts).write({
'l10n_be_is_below_scale_warning': False,
'l10n_be_is_below_scale': False
})
category_mapping = {
'A': 0,
'B': 1,
'C': 2,
'D': 3,
}
for contract in open_contracts:
company_seniority = relativedelta(fields.Date.today(), contract.first_contract_date).years
if not company_seniority:
scales = self.env['hr.rule.parameter']._get_parameter_from_code('cp200_salary_scale_first_year', raise_if_not_found=False)
else:
scales = self.env['hr.rule.parameter']._get_parameter_from_code('cp200_salary_scale', raise_if_not_found=False)
if not scales:
# No existing scale (eg: contracts before 2021)
contract.l10n_be_is_below_scale = False
contract.l10n_be_is_below_scale_warning = False
continue
anterior_seniority = contract.employee_id.l10n_be_scale_seniority
seniority = anterior_seniority + company_seniority
category_index = category_mapping.get(contract.job_id.l10n_be_scale_category, 2)
seniority_scale = scales.get(seniority, scales[26])
min_wage = seniority_scale[category_index]
if contract.time_credit:
min_wage = min_wage * contract.work_time_rate
else:
min_wage = min_wage * contract.resource_calendar_id.work_time_rate / 100
if contract._get_contract_wage() < min_wage:
contract.l10n_be_is_below_scale = True
contract.l10n_be_is_below_scale_warning = _("The wage is under the minimum scale of %s€ for a seniority of %s years.", round(min_wage, 2), seniority)
else:
contract.l10n_be_is_below_scale = False
contract.l10n_be_is_below_scale_warning = False
@api.model
def _search_l10n_be_is_below_scale(self, operator, value):
if operator not in ['=', '!='] or not isinstance(value, bool):
raise NotImplementedError(_('Operation not supported'))
below_contracts = self.env['hr.contract'].search(
[('state', 'in', ['draft', 'open'])]
).filtered(lambda c: c.company_id.country_id.code == 'BE' and c.l10n_be_is_below_scale)
if operator == '!=':
value = not value
return [('id', 'in' if value else 'not in', below_contracts.ids)]
@api.model
def _benefit_white_list(self):
return super()._benefit_white_list() + [
'insurance_amount',
'ip_value',
'l10n_be_ambulatory_insurance_amount',
'meal_voucher_paid_monthly_by_employer',
]
@api.onchange('has_hospital_insurance')
def _onchange_has_hospital_insurance(self):
if not self.has_hospital_insurance:
self.insured_relative_spouse = False
self.insured_relative_adults = 0
self.insured_relative_children = 0
@api.onchange('l10n_be_has_ambulatory_insurance')
def _onchange_l10n_be_has_ambulatory_insurance(self):
if not self.l10n_be_has_ambulatory_insurance:
self.l10n_be_ambulatory_insured_spouse = False
self.l10n_be_ambulatory_insured_adults = 0
self.l10n_be_ambulatory_insured_children = 0
@api.depends('has_hospital_insurance', 'insured_relative_adults', 'insured_relative_spouse')
def _compute_insured_relative_adults_total(self):
for contract in self:
contract.insured_relative_adults_total = (
int(contract.has_hospital_insurance)
+ contract.insured_relative_adults
+ int(contract.insured_relative_spouse))
@api.model
def _get_insurance_amount(self, child_amount, child_count, adult_amount, adult_count):
return child_amount * child_count + adult_amount * adult_count
@api.depends(
'insured_relative_children', 'insured_relative_adults_total',
'hospital_insurance_amount_per_child', 'hospital_insurance_amount_per_adult')
def _compute_insurance_amount(self):
for contract in self:
contract.insurance_amount = contract._get_insurance_amount(
contract.hospital_insurance_amount_per_child,
contract.insured_relative_children,
contract.hospital_insurance_amount_per_adult,
contract.insured_relative_adults_total)
@api.constrains('rd_percentage')
def _check_discount_percentage(self):
if self.filtered(lambda c: c.rd_percentage < 0 or c.rd_percentage > 100):
raise ValidationError(_('The time Percentage in R&D should be between 0-100'))
for contract in self:
if contract.rd_percentage and contract.employee_id.certificate not in ['civil_engineer', 'doctor', 'master', 'bachelor']:
raise ValidationError(_('Only employees with a Bachelor/Master/Doctor/Civil Engineer degree can benefit from the withholding taxes exemption.'))
@api.depends('ip', 'ip_wage_rate')
def _compute_ip_value(self):
for contract in self:
contract.ip_value = contract.ip_wage_rate if contract.ip else 0
@api.depends('commission_on_target')
def _compute_commission_cost(self):
for contract in self:
contract.warrants_cost = contract.commission_on_target * 1.326 / 1.05
warrant_commission = contract.warrants_cost * 3.0
cash_commission = contract.commission_on_target * 9.0
contract.yearly_commission_cost = warrant_commission + cash_commission * (1 + EMPLOYER_ONSS)
contract.yearly_commission = warrant_commission + cash_commission
contract.warrant_value_employee = contract.commission_on_target * 1.326 * (1.00 - 0.535)
@api.depends('meal_voucher_amount')
def _compute_meal_voucher_info(self):
# The amount of the meal voucher is computed on the basis of the contribution
# of the employer and the employee. Indeed, the first can contribute up to a
# maximum of € 6.91 per check and per day provided, while the participation
# of the second must amount to a minimum of € 1.09.
for contract in self:
contract.meal_voucher_paid_by_employer = max(0, contract.meal_voucher_amount - 1.09)
monthly_nb_meal_voucher = 220.0 / 12
contract.meal_voucher_paid_monthly_by_employer = contract.meal_voucher_paid_by_employer * monthly_nb_meal_voucher
contract.meal_voucher_average_monthly_amount = contract.meal_voucher_amount * monthly_nb_meal_voucher
@api.depends('train_transport_employee_amount')
def _compute_train_transport_reimbursed_amount(self):
for contract in self:
contract.train_transport_reimbursed_amount = contract._get_train_transport_reimbursed_amount(contract.train_transport_employee_amount)
def _get_train_transport_reimbursed_amount(self, amount):
return min(amount * 0.8, 311)
@api.depends('public_transport_employee_amount')
def _compute_public_transport_reimbursed_amount(self):
for contract in self:
contract.public_transport_reimbursed_amount = contract._get_public_transport_reimbursed_amount(contract.public_transport_employee_amount)
@api.depends('employee_id')
def _compute_employee_age(self):
for contract in self:
if not contract.employee_id or not contract.employee_id.birthday:
contract.employee_age = 0
else:
contract.employee_age = relativedelta(fields.Date.today(), contract.employee_id.birthday).years
def _get_public_transport_reimbursed_amount(self, amount):
# As of February 1st, 2020, reimbursement for non-train-based public transportation,
# when based on a flat fee, is computed as 71.8% of the actual cost, capped at the
# reimbursement for 7 km of train-based transportation (34.00 EUR)
# Source: http://www.cnt-nar.be/CCT-COORD/cct-019-09.pdf (Art. 4)
public_transport_max_amount = self.env['hr.rule.parameter'].sudo()._get_parameter_from_code(
'public_transport_max_amount', date=self.env.context.get('payslip_date'), raise_if_not_found=False)
if not public_transport_max_amount:
public_transport_max_amount = 43
return min(amount * 0.718, public_transport_max_amount)
@api.depends('km_home_work', 'transport_mode_private_car')
def _compute_private_car_reimbursed_amount(self):
for contract in self:
if contract.transport_mode_private_car:
amount = self._get_private_car_reimbursed_amount(contract.km_home_work)
else:
amount = 0.0
contract.private_car_reimbursed_amount = amount
@api.onchange('transport_mode_car', 'transport_mode_train', 'transport_mode_public')
def _onchange_transport_mode(self):
if not self.transport_mode_car:
self.fuel_card = 0
self.company_car_total_depreciated_cost = 0
if not self.transport_mode_train:
self.train_transport_reimbursed_amount = 0
if not self.transport_mode_public:
self.public_transport_reimbursed_amount = 0
if self.transport_mode_car:
self.transport_mode_private_car = False
@api.onchange('transport_mode_private_car')
def _onchange_transport_mode_private_car(self):
if self.transport_mode_private_car:
self.transport_mode_car = False
self.fuel_card = 0
@api.depends('holidays', 'wage', 'final_yearly_costs', 'l10n_be_group_insurance_rate')
def _compute_wage_with_holidays(self):
super()._compute_wage_with_holidays()
@api.depends('wage', 'l10n_be_group_insurance_rate')
def _compute_l10n_be_group_insurance_amount(self):
for contract in self:
rate = contract.l10n_be_group_insurance_rate
insurance_amount = contract.wage * rate / 100.0
contract.l10n_be_group_insurance_amount = insurance_amount
# Example
# 5 % salary configurator
# 4.4 % insurance cost
# 8.86 % ONSS
# =-----------------------
# 13.26 % over the 5%
contract.l10n_be_group_insurance_cost = insurance_amount * (1 + 13.26 / 100.0)
def _is_salary_sacrifice(self):
self.ensure_one()
return super()._is_salary_sacrifice() or self.l10n_be_group_insurance_rate
def _get_yearly_cost_sacrifice_fixed(self):
return super()._get_yearly_cost_sacrifice_fixed() + self._get_salary_costs_factor() * self.wage * self.l10n_be_group_insurance_rate / 100
@api.depends('schedule_pay')
def _compute_final_yearly_costs(self):
return super()._compute_final_yearly_costs()
def _get_salary_costs_factor(self):
self.ensure_one()
res = super()._get_salary_costs_factor()
if self.structure_type_id == self.env.ref('hr_contract.structure_type_employee_cp200'):
res = 13.92 + 13.0 * EMPLOYER_ONSS
if self.l10n_be_group_insurance_rate:
return res * (1.0 - self.l10n_be_group_insurance_rate / 100)
return res
@api.depends(
'l10n_be_has_ambulatory_insurance',
'l10n_be_ambulatory_insured_adults',
'l10n_be_ambulatory_insured_spouse')
def _compute_ambulatory_insured_adults_total(self):
for contract in self:
contract.l10n_be_ambulatory_insured_adults_total = (
int(contract.l10n_be_has_ambulatory_insurance)
+ contract.l10n_be_ambulatory_insured_adults
+ int(contract.l10n_be_ambulatory_insured_spouse))
@api.model
def _get_ambulatory_insurance_amount(self, child_amount, child_count, adult_amount, adult_count):
return child_amount * child_count + adult_amount * adult_count
@api.depends(
'l10n_be_ambulatory_insured_children', 'l10n_be_ambulatory_insured_adults_total',
'l10n_be_ambulatory_amount_per_child', 'l10n_be_ambulatory_amount_per_adult')
def _compute_ambulatory_insurance_amount(self):
for contract in self:
contract.l10n_be_ambulatory_insurance_amount = contract._get_ambulatory_insurance_amount(
contract.l10n_be_ambulatory_amount_per_child,
contract.l10n_be_ambulatory_insured_children,
contract.l10n_be_ambulatory_amount_per_adult,
contract.l10n_be_ambulatory_insured_adults_total)
@api.model
def _get_private_car_reimbursed_amount(self, distance):
# monthly train subscription amount => half is reimbursed
# Generally this is not mandatory
# See: https://emploi.belgique.be/fr/themes/remuneration/intervention-de-lemployeur-dans-les-frais-de-deplacement-domicile-lieu-de
# But this is the case for the CP200
# See: https://www.sfonds200.be/fonds-social/infos-sectorielles/frais-de-transport/prive-2020
private_car_reimbursement_scale = self.env['hr.rule.parameter'].sudo()._get_parameter_from_code(
'private_car_reimbursement_scale', date=self.env.context.get('payslip_date'), raise_if_not_found=False)
if not private_car_reimbursement_scale:
return 0
for distance_boundary, amount in private_car_reimbursement_scale:
if distance <= distance_boundary:
return amount / 2
return private_car_reimbursement_scale[-1][1] / 2
@api.model
def update_state(self):
# Called by a cron
# It sets the contract in red before the expiration of a credit time contract
date_today = fields.Date.today()
outdated_days = date_today + relativedelta(days=14)
nearly_expired_contracts = self.search([
('state', '=', 'open'),
('kanban_state', '!=', 'blocked'),
('time_credit', '=', True),
('date_end', '<', outdated_days),
])
nearly_expired_contracts.write({'kanban_state': 'blocked'})
return super().update_state()
def _preprocess_work_hours_data_split_half(self, work_data, date_from, date_to):
"""
Method is meant to be overriden, see l10n_be_hr_payroll_attendance
"""
return
def _get_work_hours_split_half(self, date_from, date_to, domain=None):
"""
Returns the amount (expressed in hours) of work
for a contract between two dates.
If called on multiple contracts, sum work amounts of each contract.
:param date_from: The start date
:param date_to: The end date
:returns: a dictionary {(half/full, work_entry_id_1): hours_1, (half/full, work_entry_id_2): hours_2}
"""
date_from = datetime.combine(date_from, datetime.min.time())
date_to = datetime.combine(date_to, datetime.max.time())
work_data = defaultdict(lambda: list([0, 0])) # [days, hours]
number_of_hours_full_day = self.resource_calendar_id._get_max_number_of_hours(date_from, date_to)
# First, found work entry that didn't exceed interval.
work_entries = self.env['hr.work.entry']._read_group(
self._get_work_hours_domain(date_from, date_to, domain=domain, inside=True),
['date_start:day', 'work_entry_type_id'],
['duration:sum']
)
self._preprocess_work_hours_data_split_half(work_entries, date_from, date_to)
for _date_start_day, work_entry_type, duration_sum in work_entries:
work_entry_type_id = work_entry_type.id
if float_compare(duration_sum, number_of_hours_full_day, 2) != -1:
if number_of_hours_full_day:
number_of_days = float_round(duration_sum / number_of_hours_full_day, precision_rounding=1, rounding_method='HALF-UP')
else:
number_of_days = 1 # If not supposed to work in calendar attendances, then there
# are not time offs
work_data[('full', work_entry_type_id)][0] += number_of_days
work_data[('full', work_entry_type_id)][1] += duration_sum
else:
work_data[('half', work_entry_type_id)][0] += 1
work_data[('half', work_entry_type_id)][1] += duration_sum
# Second, find work entry that exceeds interval and compute right duration.
work_entries = self.env['hr.work.entry'].search(self._get_work_hours_domain(date_from, date_to, domain=domain, inside=False))
for work_entry in work_entries:
date_start = max(date_from, work_entry.date_start)
date_stop = min(date_to, work_entry.date_stop)
if work_entry.work_entry_type_id.is_leave:
contract = work_entry.contract_id
calendar = contract.resource_calendar_id
employee = contract.employee_id
contract_data = employee._get_work_days_data_batch(
date_start, date_stop, compute_leaves=False, calendar=calendar
)[employee.id]
if float_compare(contract_data.get('hours', 0), number_of_hours_full_day, 2) != -1:
work_data[('full', work_entry.work_entry_type_id.id)][0] += 1
work_data[('full', work_entry.work_entry_type_id.id)][1] += work_entry.duration
else:
work_data[('half', work_entry.work_entry_type_id.id)][1] += work_entry.duration
else:
dt = date_stop - date_start
work_data[('half', work_entry.work_entry_type_id.id)] += dt.days * 24 + dt.seconds / 3600 # Number of hours
return work_data
# override to add work_entry_type from leave
def _get_leave_work_entry_type_dates(self, leave, date_from, date_to, employee):
result = super()._get_leave_work_entry_type_dates(leave, date_from, date_to, employee)
if not self._is_struct_from_country('BE'):
return result
# The public holidays are paid only during the 14 first days of unemployment
if result.code == "LEAVE500":
unemployed_less_than_14_days_before = self.env['hr.leave'].search([
('employee_id', '=', self.employee_id.id),
('date_to', '>=', leave.date_from + relativedelta(days=-14)),
('date_from', '<=', leave.date_from),
('holiday_status_id.work_entry_type_id.code', 'in', ['LEAVE6666', 'LEAVE6665']),
('state', '=', 'validate'),
], order="date_from asc")
if unemployed_less_than_14_days_before:
is_unemployed = True
for offset in range(15):
day = leave.date_from + relativedelta(days=-offset)
if all(l.date_from > day or l.date_to < day for l in unemployed_less_than_14_days_before):
is_unemployed = False
if is_unemployed:
return unemployed_less_than_14_days_before[0].holiday_status_id.work_entry_type_id
# The public holidays are paid only during the period of 30 days following the start of the
# suspension of the employment contract due to illness or accident, work accident or
# occupational disease, pregnancy or childbirth leave, strike or lockout;
if result.code == "LEAVE500":
absent_less_than_X_days_before = self.env['hr.leave'].search([
('employee_id', '=', self.employee_id.id),
('date_to', '>=', leave.date_from + relativedelta(days=-30)),
('date_from', '<=', leave.date_from),
('holiday_status_id.work_entry_type_id.code', 'in', ['LEAVE210', 'LEAVE220', 'LEAVE230', 'LEAVE115', 'LEAVE281']),
('state', '=', 'validate'),
], order="date_from asc")
if absent_less_than_X_days_before:
is_absent = True
# Special case for credit-times
# If time credit duration X is:
# X < 1 month -> Unpaid
# 1 <= X < 3 months -> Paid the first 14 days
# X >= 3 months -> Paid the first 30 days
# Alway unpaid for full time credit time
paid_duration = 30
if self.time_credit:
if not self.work_time_rate:
return absent_less_than_X_days_before[0].holiday_status_id.work_entry_type_id
duration_start = self._get_occupation_dates()[0][1]
duration_stop = leave.date_from.date()
number_of_months = (duration_stop.year - duration_start.year) * 12 + (duration_stop.month - duration_start.month)
if number_of_months < 1:
return absent_less_than_X_days_before[0].holiday_status_id.work_entry_type_id
if number_of_months < 3:
paid_duration = 14
absent_less_than_X_days_before = absent_less_than_X_days_before.filtered_domain([
('date_to', '>=', leave.date_from + relativedelta(days=-paid_duration))])
for offset in range(paid_duration):
day = leave.date_from + relativedelta(days=-offset)
if all(l.date_from > day or l.date_to < day for l in absent_less_than_X_days_before):
is_absent = False
if is_absent:
return absent_less_than_X_days_before[0].holiday_status_id.work_entry_type_id
# The salary is not guaranteed after 30 calendar days of sick leave (it means from the 31th
# day of sick leave)
# LEAVE110 = classic sick leave
if result.code == "LEAVE110":
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_less_than_30days_before = self.env['hr.leave'].search([
('employee_id', '=', self.employee_id.id),
('date_to', '>=', leave.date_from + relativedelta(days=-30)),
('date_from', '<=', leave.holiday_id.date_from),
('holiday_status_id.work_entry_type_id', 'in', sick_work_entry_types.ids),
('state', '=', 'validate'),
('id', '!=', leave.holiday_id.id),
], order="date_from asc")
if not leave.holiday_id:
return result
# The current time off is longer than 30 days -> Partial Time Off
if (date_from - leave.holiday_id.date_from).days + 1 > 30:
return partial_sick_work_entry_type
# No previous sick time off -> Sick Time Off
if not sick_less_than_30days_before:
return result
# If there a gap of more than 15 days between 2 sick time offs,
# the salary is guaranteed -> Sick Time Off
all_leaves = sick_less_than_30days_before | leave.holiday_id
for i in range(len(all_leaves) - 1):
if (all_leaves[i+1].date_from - all_leaves[i].date_to).days > 15:
return result
# No gap and more than 30 calendar days -> Partial Time Off
# only the first 30 calendar days of sickness are covered by guaranteed wages, which
# does not mean 30 days of sickness.
# Example :
# - Sick from September 1 to 7 included
# - Rework from 8 to 14
# - Re-ill from September 15 to October 13
# Here, are therefore covered by guaranteed wages:
# from 01 to 07/09 (i.e. 7 days)
# from 15/09 to 07/10 (i.e. the balance of 23 days).
# In fact, we and public holidays which fall within a period covered by a medical
# certificate are taken into account in the period of 30 calendar days of guaranteed
# salary.
# Sick days from 08/10 are therefore not covered by the employer (mutual from 08/10
# to 13/10).
total_sick_days = sum([(l.date_to - l.date_from).days + 1 for l in sick_less_than_30days_before])
this_leave_current_duration = (date_from - leave.holiday_id.date_from).days + 1
# Include off days (eg: weekends) that are not convered by time off
# in case the sick time off is split and not covering the whole
# interval
min_date_from = min(sick_less_than_30days_before.mapped('date_from'))
max_date_to = leave.holiday_id.date_to
employee_contracts = employee._get_contracts(min_date_from, max_date_to, states=['open', 'close'])
work_hours_data_by_calendar = {
c.resource_calendar_id: [work_day for work_day, work_hours in employee.with_context(
compute_leaves=False).list_work_time_per_day(min_date_from, max_date_to, c.resource_calendar_id)]
for c in employee_contracts}
effective_worked_days = {
calendar: [
work_day for work_day in work_days \
if all(work_day < l.date_from.date() or work_day > l.date_to.date() for l in sick_less_than_30days_before + leave.holiday_id)
] for calendar, work_days in work_hours_data_by_calendar.items()
}
uncovered_off_days = 0
last_seen_work_day = (min_date_from + relativedelta(days=-1)).date()
last_seen_off_day = min_date_from.date()
for day in rrule(DAILY, min_date_from, until=max_date_to):
day = day.date()
if day >= date_from.date():
continue
day_contract = employee_contracts.filtered(lambda c: c.date_start <= day and (not c.date_end or c.date_end >= day))
if day in effective_worked_days[day_contract.resource_calendar_id]:
last_seen_work_day = day
continue
if all(day < l.date_from.date() or day > l.date_to.date() for l in sick_less_than_30days_before + leave.holiday_id):
if day not in work_hours_data_by_calendar[day_contract.resource_calendar_id]:
# Backward check, only count the day if only time off before
# (eg: Avoid counting Sat/Sun if worked the days before)
if last_seen_work_day > last_seen_off_day:
continue
uncovered_off_days += 1
else:
last_seen_off_day = day
if total_sick_days + this_leave_current_duration + uncovered_off_days > 30:
return partial_sick_work_entry_type
return result
def _get_bypassing_work_entry_type_codes(self):
return super()._get_bypassing_work_entry_type_codes() + [
'LEAVE280', # Long term sick
'LEAVE281', # Partial Incapacity
# 'LEAVE110', # Sick Leave - Actually Sick Leave < Public Time Off
# If the employee does not have to work on a public
# holiday but falls ill when he could have benefited
# from a well-deserved day off, he is not entitled to
# a guaranteed salary but to remuneration in accordance
# with the days holidays. In fact, the employee is
# entitled to remuneration for each public holiday falling
# within 30 calendar days of the onset of his illness.
]
def _is_same_occupation(self, contract):
self.ensure_one()
res = super()._is_same_occupation(contract)
time_credit = self.time_credit
time_credit_type = self.time_credit_type_id
return res and time_credit == contract.time_credit and (not time_credit or (time_credit_type == contract.time_credit_type_id))
def _create_credit_time_next_activity(self):
self.ensure_one()
part_time_link = "https://www.socialsecurity.be/site_fr/employer/applics/elo/index.htm"
part_time_link = '<a href="%s" target="_blank">%s</a>' % (part_time_link, part_time_link)
self.activity_schedule(
'mail.mail_activity_data_todo',
note=_('Part Time of %s must be stated at %s.',
self.employee_id.name,
part_time_link),
user_id=self.hr_responsible_id.id or self.env.user.id,
)
def _create_dimona_next_activity(self):
self.ensure_one()
dimona_link = "https://www.socialsecurity.be/site_fr/employer/applics/dimona/index.htm"
dimona_link = '<a href="%s" target="_blank">%s</a>' % (dimona_link, dimona_link)
self.activity_schedule(
'mail.mail_activity_data_todo',
note=_('State the Dimona at %s to declare the arrival of %s.',
dimona_link,
self.employee_id.name),
user_id=self.hr_responsible_id.id or self.env.user.id,
summary='Dimona',
)
def _trigger_l10n_be_next_activities(self):
employees_with_contract_domain = [
('state', 'in', ('open', 'close')),
('employee_id', 'in', self.mapped('employee_id').ids),
('id', 'not in', self.ids),
]
employees_already_started = self.env['hr.contract'].search(employees_with_contract_domain).mapped('employee_id')
for contract in self:
if not contract._is_struct_from_country('BE'):
continue
if contract.time_credit:
contract._create_credit_time_next_activity()
if contract.employee_id not in employees_already_started:
contract._create_dimona_next_activity()
def _get_contract_insurance_amount(self, name):
self.ensure_one()
if name == 'hospital':
return self._get_hospital_insurance_amount()
if name == 'ambulatory':
return self.l10n_be_ambulatory_insurance_amount
if name == 'group':
return self.l10n_be_group_insurance_amount * (1 + 4.4 / 100.0)
return 0.0
def _get_hospital_insurance_amount(self):
self.ensure_one()
return self.insurance_amount
def write(self, vals):
res = super(HrContract, self).write(vals)
if vals.get('state') == 'open':
self._trigger_l10n_be_next_activities()
return res
@api.model_create_multi
def create(self, vals_list):
contracts = super().create(vals_list)
contracts.filtered(lambda c: c.state == 'open')._trigger_l10n_be_next_activities()
return contracts
def _get_fields_that_recompute_we(self):
return super()._get_fields_that_recompute_we() + [
'time_credit',
'time_credit_type_id',
'standard_calendar_id',
]
def _get_fields_that_recompute_payslip(self):
# Returns the fields that should recompute the payslip
return super()._get_fields_that_recompute_payslip() + [
'representation_fees',
'ip',
'ip_wage_rate',
'mobile',
'internet',
'transport_mode_car',
'transport_mode_private_car',
'transport_mode_train',
'transport_mode_public',
'train_transport_employee_amount',
'public_transport_employee_amount',
'km_home_work',
'has_laptop',
'meal_voucher_amount'
'work_time_rate',
'fiscal_voluntarism',
'fiscal_voluntary_rate',
'no_onss',
'no_withholding_taxes',
]
def action_work_schedule_change_wizard(self):
self.ensure_one()
if self.state not in ('draft', 'open'):
return False
action = self.env['ir.actions.actions']._for_xml_id('l10n_be_hr_payroll.schedule_change_wizard_action')
action['context'] = {'active_id': self.id}
return action