Files

1207 lines
66 KiB
Python

# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from datetime import date
from calendar import monthrange
from odoo import api, fields, models, Command, _
from odoo.exceptions import ValidationError, UserError
from odoo.fields import Domain
from odoo.tools.float_utils import float_round
from dateutil.relativedelta import relativedelta
SWISS_LANGUAGES = ["it_IT", "de_DE", "de_CH", "fr_FR", "fr_CH", "en_EN", "en_US"]
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
l10n_ch_social_insurance_id = fields.Many2one(
'l10n.ch.social.insurance', string="AVS/AC Insurance",
compute="_compute_l10n_ch_social_insurance_id", store=True, readonly=False)
l10n_ch_lpp_insurance_id = fields.Many2one(
'l10n.ch.lpp.insurance', string="LPP Insurance",
compute="_compute_l10n_ch_lpp_insurance_id", store=True, readonly=False)
l10n_ch_accident_insurance_line_id = fields.Many2one(
'l10n.ch.accident.insurance.line', string="AAP/AANP Insurance",
compute="_compute_l10n_ch_accident_insurance_line_id", store=True, readonly=False)
l10n_ch_additional_accident_insurance_line_ids = fields.Many2many(
'l10n.ch.additional.accident.insurance.line', string="LAAC Insurances",
compute="_compute_l10n_ch_additional_accident_insurance_line_ids", store=True, readonly=False)
l10n_ch_sickness_insurance_line_ids = fields.Many2many(
'l10n.ch.sickness.insurance.line', string="IJM Insurances",
compute="_compute_l10n_ch_sickness_insurance_line_ids", store=True, readonly=False)
l10n_ch_compensation_fund_id = fields.Many2one(
'l10n.ch.compensation.fund', string="Family Allowance (CAF)",
compute="_compute_l10n_ch_compensation_fund_id", store=True, readonly=False)
l10n_ch_after_departure_payment = fields.Selection([
('N', 'After Departure Payment'),
('NK', 'After Departure Payment with ST correction')],
string="Code change, correction-payment after departure",
help="Additional payment after leaving in the current year with maximum salary and third-party benefits")
l10n_ch_lpp_not_insured = fields.Boolean(
compute="_compute_l10n_ch_lpp_not_insured", store=True, readonly=False
)
l10n_ch_is_code = fields.Char(compute="_compute_l10n_ch_is_code", string="IS Code", store=True)
l10n_ch_is_model = fields.Selection(
string="IS Model",
selection=[('monthly', 'Monthly'), ('yearly', 'Yearly')],
compute="_compute_l10n_ch_is_model",
store=True)
l10n_ch_is_log_line_ids = fields.One2many('hr.payslip.is.log.line', 'payslip_id')
l10n_ch_avs_status = fields.Selection([
('youth', 'Youth'),
('exempted', 'Exempted'),
('retired', 'Retired'),
], string="AVS Status", compute='_compute_l10n_ch_avs_status', store=True)
l10n_ch_pay_13th_month = fields.Boolean(string="Pay Thirteen Month", compute="_compute_l10n_ch_pay_13th_month", store=True, readonly=False)
l10n_ch_laa_group = fields.Many2one("l10n.ch.accident.group", compute="_compute_l10n_ch_laa_group", store=True)
laa_solution_number = fields.Selection(selection=[
('0', '0 - Not insured'),
('1', '1 - Occupational and Non-Occupational Insured, with deductions'),
('2', '2 - Occupational and Non-Occupational Insured, without deductions'),
('3', '3 - Only Occupational Insured')], compute="_compute_laa_solution_number", store=True)
l10n_ch_location_unit_id = fields.Many2one("l10n.ch.location.unit", compute="_compute_l10n_ch_location_unit_id", store=True)
l10n_ch_txb_code = fields.Char(compute="_compute_l10n_ch_is_code", store=True)
l10n_ch_is_correction = fields.Many2one('hr.employee.is.line', compute="_compute_l10n_ch_is_correction", store=True)
l10n_ch_monthly_snapshot = fields.Many2one('l10n.ch.employee.monthly.values', compute="_compute_l10n_ch_monthly_snapshot", store=True)
l10n_ch_swiss_wage_ids = fields.One2many('l10n.ch.swiss.wage.component', 'payslip_id', compute="_compute_l10n_ch_swiss_wage_ids", store=True)
l10n_ch_entry = fields.Date(compute="_compute_l10n_ch_occupation", store=True)
l10n_ch_withdrawal = fields.Date(compute="_compute_l10n_ch_occupation", store=True)
@api.model_create_multi
def create(self, vals_list):
swiss_employees = self.env['hr.employee'].browse([val["employee_id"] for val in vals_list if "employee_id" in val]).filtered(lambda e: e.company_id.country_id.code == 'CH')
swiss_employees._create_or_update_snapshot()
return super().create(vals_list)
def _get_iso20022_communication(self, bank_account):
self.ensure_one()
bank = bank_account.bank_id
is_revolut = (bank.bic or '').upper().startswith('REVO') or 'revolut' in (bank.name or '').lower()
if is_revolut and self.company_id.country_id.code == 'CH' and bank_account in self.employee_id.bank_account_ids:
# Revolut accounts are pooled under Revolut's own IBANs: the beneficiary's name
# and country must appear in the communication so the payment can be credited
# to the right account.
return f'{self.employee_id.l10n_ch_legal_first_name} {self.employee_id.l10n_ch_legal_last_name}, CH'
return super()._get_iso20022_communication(bank_account)
def _get_schedule_timedelta(self):
self.ensure_one()
if self.struct_id.code == "CHMONTHLYELM":
return relativedelta(months=1, days=-1)
else:
return super()._get_schedule_timedelta()
@api.model
def _issues_dependencies(self):
return super()._issues_dependencies() + ['l10n_ch_monthly_snapshot.validation_errors']
def _get_warnings_by_slip(self):
warnings_by_slip = super()._get_warnings_by_slip()
for slip in self:
if warnings := slip.l10n_ch_monthly_snapshot.validation_errors:
warnings_by_slip[slip].extend(warnings.values())
return warnings_by_slip
@api.depends('date_from', 'struct_id')
def _compute_date_to(self):
swissdec_slips = self.filtered(lambda p: p.struct_id.code == 'CHMONTHLYELM')
for payslip in swissdec_slips:
if payslip.struct_id.code == "CHMONTHLYELM":
payslip.date_to = payslip.date_from + payslip._get_schedule_timedelta()
super(HrPayslip, self - swissdec_slips)._compute_date_to()
@api.depends('version_id.l10n_ch_social_insurance_id', 'state')
def _compute_l10n_ch_social_insurance_id(self):
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated']:
continue
payslip.l10n_ch_social_insurance_id = payslip.version_id.l10n_ch_social_insurance_id
@api.depends('version_id.l10n_ch_lpp_insurance_id', 'state')
def _compute_l10n_ch_lpp_insurance_id(self):
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated']:
continue
payslip.l10n_ch_lpp_insurance_id = payslip.version_id.l10n_ch_lpp_insurance_id
@api.depends('version_id.l10n_ch_accident_insurance_line_id', 'state')
def _compute_l10n_ch_accident_insurance_line_id(self):
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated']:
continue
payslip.l10n_ch_accident_insurance_line_id = payslip.version_id.l10n_ch_accident_insurance_line_id
@api.depends('version_id.l10n_ch_additional_accident_insurance_line_ids', 'state')
def _compute_l10n_ch_additional_accident_insurance_line_ids(self):
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated']:
continue
payslip.l10n_ch_additional_accident_insurance_line_ids = [(6, 0, payslip.version_id.l10n_ch_additional_accident_insurance_line_ids.ids)]
@api.depends('version_id.l10n_ch_sickness_insurance_line_ids', 'state')
def _compute_l10n_ch_sickness_insurance_line_ids(self):
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated']:
continue
payslip.l10n_ch_sickness_insurance_line_ids = [(6, 0, payslip.version_id.l10n_ch_sickness_insurance_line_ids.ids)]
@api.depends('version_id.l10n_ch_compensation_fund_id', 'state')
def _compute_l10n_ch_compensation_fund_id(self):
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated']:
continue
payslip.l10n_ch_compensation_fund_id = payslip.version_id.l10n_ch_compensation_fund_id
@api.depends('payslip_run_id.l10n_ch_pay_13th_month')
def _compute_l10n_ch_pay_13th_month(self):
payslip_to_recompute = self.env['hr.payslip']
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated'] or not payslip.payslip_run_id:
continue
if payslip.l10n_ch_pay_13th_month != payslip.payslip_run_id.l10n_ch_pay_13th_month:
if payslip.state == "waiting":
payslip_to_recompute += payslip
payslip.l10n_ch_pay_13th_month = payslip.payslip_run_id.l10n_ch_pay_13th_month
if payslip_to_recompute:
payslip_to_recompute.compute_sheet()
@api.depends('version_id.l10n_ch_avs_status')
def _compute_l10n_ch_avs_status(self):
for payslip in self:
contract = payslip.version_id
if payslip.state != 'draft' or self.env.context.get('ch_skip_payslip_update'):
continue
payslip.l10n_ch_avs_status = contract.l10n_ch_avs_status
@api.depends("l10n_ch_is_code")
def _compute_l10n_ch_is_model(self):
for payslip in self:
if payslip.struct_id.code != "CHMONTHLYELM":
continue
if payslip.l10n_ch_is_code:
st_canton = payslip.l10n_ch_is_code.split("-")[0]
if st_canton in ["GE", "FR", "TI", "VS", "VD"]:
payslip.l10n_ch_is_model = "yearly"
else:
payslip.l10n_ch_is_model = "monthly"
else:
payslip.l10n_ch_is_model = False
@api.depends('version_id.l10n_ch_lpp_not_insured', 'state')
def _compute_l10n_ch_lpp_not_insured(self):
for payslip in self:
if payslip.company_id.country_id.code != "CH" or payslip.state in ['paid', 'validated']:
continue
payslip.l10n_ch_lpp_not_insured = payslip.version_id.l10n_ch_lpp_not_insured
def _get_payslip_line_total(self, amount, quantity, rate, rule):
total = super()._get_payslip_line_total(amount, quantity, rate, rule)
if self.company_id.country_id.code != "CH" or not rule.l10n_ch_5_cents_rounding:
return total
return float_round(total, precision_rounding=0.05, rounding_method="HALF-UP")
def _filter_not_in_contract_payslips(self):
return super()._filter_not_in_contract_payslips().filtered(lambda p: p.struct_id.country_id.code != "CH")
def _get_contract_days_in_payslip_range(self, date_start, date_end):
"""
Return the full count of actual calendar days in the payslip
for which the employee is under contract.
"""
self.ensure_one()
if not date_start:
return 0
if self.company_id.l10n_ch_30_day_method:
return self._l10n_ch_get_occupation_days(date_start, date_end)
payslip_start = self.date_from
payslip_end = self.date_to
contract_start = date_start
contract_end = date_end or payslip_end
overlap_start = max(payslip_start, contract_start)
overlap_end = min(payslip_end, contract_end)
if overlap_start > overlap_end:
return 0
return (overlap_end - overlap_start).days + 1
def _get_payroll_impacting_swissdec(self):
leave_type_refs = [
'hr_holidays.l10n_ch_swissdec_unpaid_lt',
'hr_holidays.l10n_ch_swissdec_illness_lt',
'hr_holidays.l10n_ch_swissdec_accident_lt',
'hr_holidays.l10n_ch_swissdec_maternity_lt',
'hr_holidays.l10n_ch_swissdec_military_lt',
'hr_holidays.l10n_ch_swissdec_interruption_of_work_lt',
]
valid_leave_types = [self.env.ref(ref, raise_if_not_found=False) for ref in leave_type_refs]
valid_leave_types_ids = [lt.id for lt in valid_leave_types if lt]
return self.env['hr.leave.type'].browse(valid_leave_types_ids)
@api.depends('employee_id', 'version_id', 'struct_id', 'date_from', 'date_to', 'l10n_ch_entry', 'l10n_ch_withdrawal')
def _compute_l10n_ch_swiss_wage_ids(self):
swissdec_slips = self.filtered(lambda p: p.struct_id.code == 'CHMONTHLYELM')
if not swissdec_slips:
return
swissdec_slips.update({'l10n_ch_swiss_wage_ids': [(5, 0, 0)]})
payroll_impacting_leave_types = self._get_payroll_impacting_swissdec()
leaves_grouped_by_employee = dict(self.env['hr.leave']._read_group(
domain=[('employee_id', 'in', swissdec_slips.mapped('employee_id').ids),
('state', 'in', ['validate', 'validate1']),
('holiday_status_id', 'in', payroll_impacting_leave_types.ids)],
groupby=['employee_id'],
aggregates=['id:recordset']
))
grouped_recurring_hours = dict(self.env['l10n.ch.hr.contract.wage']._read_group(domain=[('version_id', 'in', swissdec_slips.version_id.ids),
('date_start', '=', False),
('uom', '=', 'hours')], groupby=['version_id'], aggregates=['id:recordset']))
grouped_one_time_hours = self.env['l10n.ch.hr.contract.wage']._read_group(domain=[('version_id', 'in', swissdec_slips.version_id.ids),
('date_start', '!=', False),
('uom', '=', 'hours')], groupby=['version_id', 'date_start:year', 'date_start:month'], aggregates=['id:recordset'])
grouped_one_time_wages_dict = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: self.env['l10n.ch.hr.contract.wage'])))
for contract, date_y, date_m, wage in grouped_one_time_hours:
grouped_one_time_wages_dict[contract][date_y.year][date_m.month] += wage
monthly_work_entry = self.env.ref('hr_work_entry.l10n_ch_swissdec_monthly_wt', raise_if_not_found=False)
hourly_work_entry = self.env.ref('hr_work_entry.l10n_ch_swissdec_hourly_wt', raise_if_not_found=False)
lesson_work_entry = self.env.ref('hr_work_entry.l10n_ch_swissdec_lesson_wt', raise_if_not_found=False)
overtime_work_entry = self.env.ref('hr_work_entry.l10n_ch_swissdec_overtime_wt', raise_if_not_found=False)
overtime_125_work_entry = self.env.ref('hr_work_entry.l10n_ch_swissdec_overtime_125_wt', raise_if_not_found=False)
overtime_150_work_entry = self.env.ref('l10n_ch_hr_payroll.l10n_ch_swissdec_overtime_150_wt', raise_if_not_found=False)
overtime_200_work_entry = self.env.ref('l10n_ch_hr_payroll.l10n_ch_swissdec_overtime_200_wt', raise_if_not_found=False)
on_call_duty_125_work_entry = self.env.ref('l10n_ch_hr_payroll.l10n_ch_swissdec_oncall_125_wt', raise_if_not_found=False)
night_shift_110_work_entry = self.env.ref('l10n_ch_hr_payroll.l10n_ch_swissdec_night_110_wt', raise_if_not_found=False)
mapped_hourly_absence = {
"CH_ACCIDENT": self.env.ref('hr_work_entry.l10n_ch_swissdec_accident_wt_hourly', raise_if_not_found=False),
"CH_ILLNESS": self.env.ref('hr_work_entry.l10n_ch_swissdec_illness_wt_hourly', raise_if_not_found=False),
"CH_MATERNITY": self.env.ref('hr_work_entry.l10n_ch_swissdec_maternity_wt_hourly', raise_if_not_found=False),
"CH_MILITARY": self.env.ref('hr_work_entry.l10n_ch_swissdec_military_wt_hourly', raise_if_not_found=False),
}
for payslip in swissdec_slips:
if not payslip.version_id:
continue
reccurent_wage_types = grouped_recurring_hours.get(payslip.version_id, self.env['l10n.ch.hr.contract.wage'])
one_time_wage_types = grouped_one_time_wages_dict[payslip.version_id][payslip.date_to.year][payslip.date_to.month]
grouped_recurring_by_input_wage_types = reccurent_wage_types.grouped(lambda w: w.input_type_id.code)
grouped_one_time_by_input_wage_types = one_time_wage_types.grouped(lambda w: w.input_type_id.code)
worked_day_vals = []
base_days = payslip._get_contract_days_in_payslip_range(payslip.l10n_ch_entry, payslip.l10n_ch_withdrawal)
total_days = payslip.date_to.day
if self.company_id.l10n_ch_30_day_method:
total_days = 30
contract = payslip.version_id
has_monthly = contract.l10n_ch_has_monthly
has_hourly = contract.l10n_ch_has_hourly
has_lesson = contract.l10n_ch_has_lesson
monthly_wage = contract.wage
hourly_wage = contract.hourly_wage
lesson_wage = contract.l10n_ch_lesson_wage
range_min = max(payslip.date_from, contract.date_start)
range_max = min(payslip.date_to, contract.date_end or payslip.date_to)
leaves = leaves_grouped_by_employee.get(payslip.employee_id, self.env['hr.leave']).filtered(lambda l: l.request_date_from <= range_max and l.request_date_to >= range_min)
leave_days_map = {}
for leave in leaves:
overlap_start = max(range_min, leave.request_date_from)
overlap_end = min(range_max, leave.request_date_to)
overlap_days = payslip._get_contract_days_in_payslip_range(overlap_start, overlap_end)
leave_type = leave.holiday_status_id
leave_days_map.setdefault(leave_type, [])
if leave.request_date_from_period == 'pm':
overlap_days -= 0.5
if leave.request_date_to_period == 'am':
overlap_days -= 0.5
leave_days_map[leave_type] += [(overlap_days, leave)]
total_leave_days = 0.0
for leave_type, all_leaves in leave_days_map.items():
for leave in all_leaves:
nb_days = leave[0]
leave_id = leave[1]
work_entry_type = leave_type.work_entry_type_id
if work_entry_type.code == "CH_Interruption":
disability_percentage = 1
continued_pay = 0
else:
disability_percentage = leave_id.l10n_ch_disability_percentage
continued_pay = leave_id.l10n_ch_continued_pay_percentage
proportion_in_classic_wage = 1 - disability_percentage
if has_monthly:
if proportion_in_classic_wage > 0:
worked_day_vals += [(0, 0, {
'name': f"{monthly_work_entry.name} : {nb_days} / {total_days}",
'sequence': 5,
'work_entry_type_id': monthly_work_entry.id,
'salary_base': monthly_wage * proportion_in_classic_wage,
'rate': nb_days / total_days
})]
if (has_hourly or has_lesson) and work_entry_type.code in mapped_hourly_absence:
worked_day_vals += [(0, 0, {
'name': f"{mapped_hourly_absence[work_entry_type.code].name} : {nb_days} / {total_days}",
'sequence': 5,
'work_entry_type_id': mapped_hourly_absence[work_entry_type.code].id,
'salary_base': hourly_wage or lesson_wage,
'rate': 0
})]
if has_monthly:
worked_day_vals += [(0, 0, {
'name': f"{work_entry_type.name} : {nb_days} / {total_days}",
'sequence': 10,
'work_entry_type_id': work_entry_type.id,
'salary_base': monthly_wage * disability_percentage * nb_days / total_days if has_monthly else 0,
'rate': continued_pay,
})]
total_leave_days += nb_days
regular_days = base_days - total_leave_days
if regular_days < 0:
regular_days = 0.0
if has_monthly:
worked_day_vals += [(0, 0, {
'name': f"{monthly_work_entry.name} : {regular_days} / {total_days}",
'sequence': 5,
'work_entry_type_id': monthly_work_entry.id,
'salary_base': monthly_wage,
'rate': regular_days / total_days,
})]
if grouped_recurring_by_input_wage_types.get("WT_Hours", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': hourly_work_entry.id,
'salary_base': hourly_wage,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_Hours").mapped('amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_Hours", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': hourly_work_entry.id,
'salary_base': hourly_wage,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_Hours").mapped('amount')),
})]
if not (grouped_recurring_by_input_wage_types.get("WT_Hours", False) or grouped_one_time_by_input_wage_types.get("WT_Hours", False)) and has_hourly:
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': hourly_work_entry.id,
'salary_base': hourly_wage,
'rate': 0,
})]
if grouped_recurring_by_input_wage_types.get("WT_Overtime", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_work_entry.id,
'salary_base': hourly_wage,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_Overtime").mapped('amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_Overtime", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_work_entry.id,
'salary_base': hourly_wage,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_Overtime").mapped('amount')),
})]
if grouped_recurring_by_input_wage_types.get("WT_Overtime_125", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_125_work_entry.id,
'salary_base': hourly_wage * 1.25,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_Overtime_125").mapped(
'amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_Overtime_125", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_125_work_entry.id,
'salary_base': hourly_wage * 1.25,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_Overtime_125").mapped('amount')),
})]
if grouped_recurring_by_input_wage_types.get("WT_Overtime_150", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_150_work_entry.id,
'salary_base': hourly_wage * 1.5,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_Overtime_150").mapped(
'amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_Overtime_150", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_150_work_entry.id,
'salary_base': hourly_wage * 1.5,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_Overtime_150").mapped('amount')),
})]
if grouped_recurring_by_input_wage_types.get("WT_Overtime_200", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_200_work_entry.id,
'salary_base': hourly_wage * 2.0,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_Overtime_200").mapped(
'amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_Overtime_200", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': overtime_200_work_entry.id,
'salary_base': hourly_wage * 2.0,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_Overtime_200").mapped('amount')),
})]
if grouped_recurring_by_input_wage_types.get("WT_on_call_125", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': on_call_duty_125_work_entry.id,
'salary_base': hourly_wage * 1.25,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_on_call_125").mapped(
'amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_on_call_125", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': on_call_duty_125_work_entry.id,
'salary_base': hourly_wage * 1.25,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_on_call_125").mapped('amount')),
})]
if grouped_recurring_by_input_wage_types.get("WT_night_110", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': night_shift_110_work_entry.id,
'salary_base': hourly_wage * 1.1,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_night_110").mapped(
'amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_night_110", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': night_shift_110_work_entry.id,
'salary_base': hourly_wage * 1.1,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_night_110").mapped('amount')),
})]
if grouped_recurring_by_input_wage_types.get("WT_Lesson_input", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': lesson_work_entry.id,
'salary_base': lesson_wage,
'rate': sum(grouped_recurring_by_input_wage_types.get("WT_Lesson_input").mapped(
'amount')) * base_days / total_days,
})]
if grouped_one_time_by_input_wage_types.get("WT_Lesson_input", False):
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': lesson_work_entry.id,
'salary_base': lesson_wage,
'rate': sum(grouped_one_time_by_input_wage_types.get("WT_Lesson_input").mapped(
'amount'))
})]
if not (grouped_recurring_by_input_wage_types.get("WT_Lesson_input", False) or grouped_one_time_by_input_wage_types.get("WT_Lesson_input", False)) and has_lesson:
worked_day_vals += [(0, 0, {
'sequence': 15,
'work_entry_type_id': lesson_work_entry.id,
'salary_base': lesson_wage,
'rate': 0,
})]
payslip.update({
"l10n_ch_swiss_wage_ids": worked_day_vals
})
@api.depends('employee_id', "date_from", "date_to")
def _compute_l10n_ch_monthly_snapshot(self):
swiss_payslips = self.filtered(lambda p: p.struct_id.code == "CHMONTHLYELM")
if not swiss_payslips:
return
mapped_snapshots = self.env['l10n.ch.employee.yearly.values']._get_mapped_snapshots(
domain=[('employee_id', 'in', swiss_payslips.mapped('employee_id').ids)]
)
slips_grouped_by_date = swiss_payslips.grouped('date_to')
new_snapshots_created = False
for date_group, slips in slips_grouped_by_date.items():
year = date_group.year
month = date_group.month
employees_missing_snapshot = slips.employee_id.filtered(
lambda e: not mapped_snapshots[e][year][month]
)
if employees_missing_snapshot:
employees_missing_snapshot.with_context(l10n_ch_reference_date=date_group)._create_or_update_snapshot()
new_snapshots_created = True
if new_snapshots_created:
mapped_snapshots = self.env['l10n.ch.employee.yearly.values']._get_mapped_snapshots(
domain=[('employee_id', 'in', swiss_payslips.mapped('employee_id').ids)]
)
for payslip in swiss_payslips:
month = payslip.date_to.month
year = payslip.date_to.year
payslip.l10n_ch_monthly_snapshot = mapped_snapshots[payslip.employee_id][year][month]
@api.depends('employee_id')
def _compute_l10n_ch_is_correction(self):
swiss_payslips = self.filtered(lambda p: p.struct_id.code == "CHMONTHLYELM")
if not swiss_payslips:
return
grouped_pending_corrections = dict(self.env['hr.employee.is.line']._read_group(
domain=[('employee_id', 'in', swiss_payslips.employee_id.ids),
('state', '=', 'pending')],
groupby=['employee_id'], aggregates=['id:recordset']))
for payslip in swiss_payslips:
pending_c = grouped_pending_corrections.get(payslip.employee_id, False)
if pending_c:
payslip.l10n_ch_is_correction = pending_c[-1]
else:
payslip.l10n_ch_is_correction = False
@api.depends('line_ids.total')
def _compute_basic_net(self):
elm_slips = self.filtered(lambda p: p.struct_id.code == "CHMONTHLYELM")
if not elm_slips:
return super()._compute_basic_net()
line_values = (self._origin)._get_line_values(["WT_1000", "BASICHOURLY", "BASICLESSON", "GROSS_SALARY", 'NET', 'Net_Paid'])
employer_cost_codes = set(self.env['hr.salary.rule'].search([
('appears_on_employee_cost_dashboard', '=', True),
('struct_id.code', '=', 'CHMONTHLYELM')
]).mapped('code'))
employer_cost_values = {}
if employer_cost_codes:
employer_cost_values = (self._origin)._get_line_values(employer_cost_codes)
for payslip in elm_slips:
employer_cost_total = 0.0
payslip_employer_codes = payslip.struct_id.rule_ids.filtered(
'appears_on_employee_cost_dashboard'
).mapped('code')
for code in payslip_employer_codes:
employer_cost_total += employer_cost_values[code][payslip._origin.id]['total']
payslip.basic_wage = line_values['WT_1000'][payslip._origin.id]['total'] + line_values['BASICHOURLY'][payslip._origin.id]['total'] + line_values['BASICLESSON'][payslip._origin.id]['total']
payslip.gross_wage = line_values['GROSS_SALARY'][payslip._origin.id]['total']
payslip.net_wage = line_values['NET'][payslip._origin.id]['total'] + line_values['Net_Paid'][payslip._origin.id]['total']
payslip.employer_cost = employer_cost_total
super(HrPayslip, self - elm_slips)._compute_basic_net()
def refund_sheet(self):
if any(payslip.struct_id.country_id.code == "CH" for payslip in self):
raise UserError(_(
"Refunds are not supported for Swiss payroll as only one payslip per month is permitted. "
"Please cancel this payslip instead and generate a new one to apply corrections."
))
return super().refund_sheet()
def _get_base_local_dict(self):
res = super()._get_base_local_dict()
if self.struct_id.code == "CHMONTHLYELM":
date_from = date(self.date_from.year, 1, 1)
date_to = self.date_from
wage_components = defaultdict(lambda: {
"base": 0,
"rate": 0,
"total": 0
})
for work_entry_type, wages in self.l10n_ch_swiss_wage_ids.grouped("work_entry_type_id").items():
if len(wages) == 1:
# We can display base and rate since the components are made of one line
wage_components[work_entry_type.code]["base"] = wages.salary_base
wage_components[work_entry_type.code]["rate"] = wages.rate * 100
wage_components[work_entry_type.code]["total"] = wages.amount
else:
total_sum = sum(wages.mapped('amount'))
wage_components[work_entry_type.code]["total"] = total_sum
res.update({
'ch_wage_components': wage_components,
})
if self.l10n_ch_after_departure_payment:
reference_payslip = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('state', 'in', ['validated', 'paid']),
('struct_id.code', '=', 'CHMONTHLYELM'),
('l10n_ch_after_departure_payment', '=', False),
], order="date_from DESC", limit=1)
if not reference_payslip:
raise ValidationError(_("A previous payslip in the system is required for after departure payement to work."))
if reference_payslip.date_from.year != self.date_from.year:
date_from = date(reference_payslip.date_from.year, 1, 1)
date_to = self.date_from
res.update({
'reference_is_slip': reference_payslip
})
res.update({
'previous_payslips': self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('date_from', '>=', date_from),
('date_to', '<', date_to),
('state', 'in', ['validated', 'paid']),
('struct_id.code', '=', 'CHMONTHLYELM'),
]),
})
return res
@api.depends('version_id.l10n_ch_location_unit_id')
def _compute_l10n_ch_location_unit_id(self):
for payslip in self:
if payslip.state != 'draft' or payslip.company_id.country_id.code != "CH":
continue
payslip.l10n_ch_location_unit_id = payslip.version_id.l10n_ch_location_unit_id
@api.depends('version_id.l10n_ch_laa_group')
def _compute_l10n_ch_laa_group(self):
for payslip in self:
if payslip.state != 'draft' or payslip.company_id.country_id.code != "CH":
continue
payslip.l10n_ch_laa_group = payslip.version_id.l10n_ch_laa_group
@api.depends('version_id.laa_solution_number')
def _compute_laa_solution_number(self):
for payslip in self:
if payslip.state != 'draft' or payslip.company_id.country_id.code != "CH":
continue
payslip.laa_solution_number = payslip.version_id.laa_solution_number
def compute_sheet(self):
# Complete compute Sheet override to avoid any overrides from other apps
payslips = self.filtered(lambda slip: slip.state == 'draft' and slip.struct_id.code == "CHMONTHLYELM")
if not payslips:
return super().compute_sheet()
payslips.line_ids.unlink()
payslips.l10n_ch_is_log_line_ids.unlink()
payslips._compute_l10n_ch_is_correction()
payslips._compute_l10n_ch_is_code()
payslips._compute_l10n_ch_is_model()
payslips._compute_issues()
self.env.flush_all()
today = fields.Date.today()
for payslip in payslips:
payslip.write({
'compute_date': today
})
self.env['hr.payslip.line'].create(payslips._get_payslip_lines())
super(HrPayslip, self - payslips).compute_sheet()
def action_payslip_done(self):
res = super().action_payslip_done()
swiss_payslips = self.filtered(lambda p: p.struct_id.country_id.code == "CH")
if not swiss_payslips:
return res
swiss_payslips.l10n_ch_is_correction.action_done()
for ref_date, payslips_by_date in swiss_payslips.grouped('date_from').items():
payslips_by_date.employee_id.with_context(
update_salaries=True,
l10n_ch_reference_date=ref_date
)._create_or_update_snapshot()
return res
def action_payslip_cancel(self):
res = super().action_payslip_cancel()
swiss_payslips = self.filtered(lambda p: p.struct_id.country_id.code == "CH")
if not swiss_payslips:
return res
swiss_payslips.l10n_ch_is_correction.action_pending()
swiss_payslips.l10n_ch_is_log_line_ids.unlink()
for ref_date, payslips_by_date in swiss_payslips.grouped('date_from').items():
payslips_by_date.employee_id.with_context(
update_salaries=True,
unlock_pay_period=True,
l10n_ch_reference_date=ref_date
)._create_or_update_snapshot()
return res
@api.depends('employee_id', 'l10n_ch_monthly_snapshot')
def _compute_l10n_ch_is_code(self):
for payslip in self:
if payslip.state != 'draft' or payslip.company_id.country_id.code != "CH":
continue
valid_source_tax_snapshot = payslip.l10n_ch_monthly_snapshot
source_tax_code = False
txb_code = False
if valid_source_tax_snapshot.employee_meta_data:
source_tax_canton = valid_source_tax_snapshot.employee_meta_data.get("st-canton", False)
source_tax_scale = valid_source_tax_snapshot.employee_meta_data.get("st-code", False)
source_tax_municipality = valid_source_tax_snapshot.employee_meta_data.get("st-municipality", False)
txb_code = valid_source_tax_snapshot.employee_meta_data.get("txb-code", False)
if source_tax_canton and source_tax_scale and source_tax_municipality:
source_tax_code = f"{source_tax_canton}-{source_tax_scale}-{source_tax_municipality}"
payslip.l10n_ch_is_code = source_tax_code
payslip.l10n_ch_txb_code = txb_code
def _has_lpp_in_percentage(self):
# To be overriden in ELM 5.3 certification module
self.ensure_one()
return False
@api.depends('employee_id', 'version_id', 'struct_id', 'date_from', 'date_to', 'struct_id')
def _compute_input_line_ids(self):
swiss_slips = self.filtered(lambda p: p.struct_id.code == "CHMONTHLYELM")
if not swiss_slips:
return super()._compute_input_line_ids()
payroll_impacting_leave_types = self._get_payroll_impacting_swissdec()
work_interruption_type = self.env.ref('hr_holidays.l10n_ch_swissdec_interruption_of_work_lt', raise_if_not_found=False)
swiss_slips.update({'input_line_ids': [(5, 0, 0)]})
leaves_grouped_by_employee = dict(self.env['hr.leave']._read_group(
domain=[('employee_id', 'in', swiss_slips.mapped('employee_id').ids),
('state', 'in', ['validate', 'validate1']),
('holiday_status_id', 'in', payroll_impacting_leave_types.ids)],
groupby=['employee_id'],
aggregates=['id:recordset']
))
lpp_input = self.env.ref('l10n_ch_hr_payroll.l10n_ch_hr_elm_input_WT_5050')
lpp_comp_input = self.env.ref('l10n_ch_hr_payroll.l10n_ch_hr_elm_input_WT_7050')
grouped_recurring_wages = dict(self.env['l10n.ch.hr.contract.wage']._read_group(domain=[('version_id', 'in', swiss_slips.version_id.ids),
('date_start', '=', False),
('uom', '!=', 'hours')], groupby=['version_id'], aggregates=['id:recordset']))
grouped_one_time_wages = self.env['l10n.ch.hr.contract.wage']._read_group(domain=[('version_id', 'in', swiss_slips.version_id.ids),
('date_start', '!=', False),
('uom', '!=', 'hours')], groupby=['version_id', 'date_start:year', 'date_start:month'], aggregates=['id:recordset'])
grouped_one_time_wages_dict = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: self.env['l10n.ch.hr.contract.wage'])))
for contract, date_y, date_m, wage in grouped_one_time_wages:
grouped_one_time_wages_dict[contract][date_y.year][date_m.month] += wage
for slip in swiss_slips:
if not slip.version_id:
continue
range_min = max(slip.date_from, slip.version_id.date_start)
range_max = min(slip.date_to, slip.version_id.date_end or slip.date_to)
has_pay_interruption = leaves_grouped_by_employee.get(slip.employee_id, self.env['hr.leave']).filtered(lambda l: l.date_from.date() <= range_max and l.date_to.date() >= range_min and l.holiday_status_id.id == work_interruption_type.id)
input_line_vals = []
wage_types = grouped_one_time_wages_dict[slip.version_id][slip.date_to.year][slip.date_to.month]
recurring_wage_types = grouped_recurring_wages.get(slip.version_id, self.env['l10n.ch.hr.contract.wage'])
has_lpp_in_percentage = slip._has_lpp_in_percentage()
if not slip.l10n_ch_lpp_not_insured and slip.l10n_ch_lpp_insurance_id and not slip.l10n_ch_after_departure_payment and not has_pay_interruption.l10n_ch_lpp_interruption and not has_lpp_in_percentage:
if slip.version_id.lpp_employee_amount:
input_line_vals.append(Command.create({
'amount': slip.version_id.lpp_employee_amount,
'input_type_id': lpp_input.id,
}))
if slip.version_id.lpp_company_amount:
input_line_vals.append(Command.create({
'amount': slip.version_id.lpp_company_amount,
'input_type_id': lpp_comp_input.id,
}))
for wage_type in wage_types:
input_line_vals.append(Command.create({
'name': wage_type.description,
'amount': wage_type.amount,
'input_type_id': wage_type.input_type_id.id,
}))
if not has_pay_interruption.l10n_ch_pay_interruption:
for wage_type in recurring_wage_types:
input_line_vals.append(Command.create({
'name': wage_type.description,
'amount': wage_type.amount,
'input_type_id': wage_type.input_type_id.id,
}))
input_line_vals += slip._get_additional_input_line_vals()
slip.update({'input_line_ids': input_line_vals})
super(HrPayslip, self - swiss_slips)._compute_input_line_ids()
def _get_additional_input_line_vals(self):
# To be overriden in additional Swiss modules
return []
def _reverse_log_lines(self, payslip_to_reverse, manual_correction=None):
# Reversal, this requires heavy logic since one payslip could be corrected multiple times through various payslips
# The returned result is the total compensation
self.ensure_one()
log_lines = self.env['hr.payslip.is.log.line'].search(['|', '&', ('payslip_id', '=', payslip_to_reverse.id), ('is_correction', '=', False), '&', ('corrected_slip_id', '=', payslip_to_reverse.id), ('correction_type', '=', 'new')]).sorted(lambda l: l.payslip_id.date_to)
total_compensation = 0
if log_lines:
grouped_lines = log_lines.grouped('payslip_id')
old_grouped_lines_key = max(grouped_lines, key=lambda p: p.date_to)
old_grouped_lines = grouped_lines[old_grouped_lines_key]
old_grouped_lines_by_code = old_grouped_lines.grouped('code')
valid_code = old_grouped_lines_by_code['ASDAYS'].is_code
valid_canton = old_grouped_lines_by_code['ASDAYS'].source_tax_canton
valid_municipality = old_grouped_lines_by_code['ASDAYS'].source_tax_municipality
for line in old_grouped_lines:
self._log_is_line(is_canton=valid_canton, is_code=valid_code, municipality=valid_municipality, code=line.code, amount=-line.amount, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='old')
if line.code == 'IS':
total_compensation += line.amount
if manual_correction:
new_canton = manual_correction.l10n_ch_source_tax_canton
new_municipality = manual_correction.l10n_ch_source_tax_municipality
new_code = manual_correction.tax_code
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ASDAYS', amount=manual_correction.insurance_days, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISWORKEDDAYS', amount=manual_correction.worked_days, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISWORKEDDAYSINCH', amount=manual_correction.worked_days_in_switzerland, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARYAPERIODIC', amount=manual_correction.source_tax_periodic_determinant_salary, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARYPERIODIC', amount=manual_correction.source_tax_aperiodic_determinant_salary, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISSALARY', amount=manual_correction.source_tax_salary, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARY', amount=manual_correction.rate_determinant_salary, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='IS', amount=manual_correction.source_tax_amount, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
total_compensation += manual_correction.source_tax_amount
else:
new_canton, new_code, new_municipality = self.l10n_ch_is_code.split('-')
old_as_days = old_grouped_lines_by_code.get('ASDAYS', self.env['hr.payslip.is.log.line']).amount
old_is_days = old_grouped_lines_by_code.get('ISWORKEDDAYS', self.env['hr.payslip.is.log.line']).amount
old_is_ch_days = old_grouped_lines_by_code.get('ISWORKEDDAYSINCH', self.env['hr.payslip.is.log.line']).amount
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ASDAYS', amount=old_as_days, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISWORKEDDAYS', amount=old_is_days, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISWORKEDDAYSINCH', amount=old_is_ch_days, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
previous_payslips = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('date_from', '>=', date(payslip_to_reverse.date_from.year, 1, 1)),
('date_to', '<', payslip_to_reverse.date_to),
('state', 'in', ['validated', 'paid']),
('struct_id.code', '=', 'CHMONTHLYELM'),
])
if new_code in ['NON', 'NOY']:
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISSALARY', amount=0, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARY', amount=0, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='IS', amount=0, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
else:
line_values = payslip_to_reverse._get_line_values(['ISSALARYDTP', 'ISSALARYDTAP', '13THMONTH', '14THMONTH', '13THMONTH_HOURLY', '14THMONTH_HOURLY', 'ACTIVITYRATE', 'ACTIVITYRATETOTAL', 'ISWORKEDDAYSINCH', 'ISWORKEDDAYS'], compute_sum=True)
is_dt_salary_aperiodic = line_values['ISSALARYDTAP']['sum']['total']
is_dt_salary_periodic = line_values['ISSALARYDTP']['sum']['total'] / (line_values['ACTIVITYRATE']['sum']['total'] / 100) * (line_values['ACTIVITYRATETOTAL']['sum']['total'] / 100)
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARYAPERIODIC', amount=is_dt_salary_aperiodic, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARYPERIODIC', amount=is_dt_salary_periodic, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
log_lines = (previous_payslips + payslip_to_reverse)._get_is_log_lines(compute_total=True)["total"][new_canton]
year_days = log_lines['ISWORKEDDAYS']
year_days_ch = log_lines['ISWORKEDDAYSINCH']
# ISSALARY
thirteen_m = line_values['13THMONTH']['sum']['total'] + line_values['13THMONTH_HOURLY']['sum']['total'] + line_values['14THMONTH']['sum']['total'] + line_values['14THMONTH_HOURLY']['sum']['total']
salary_month = (line_values['ISSALARYDTP']['sum']['total'] - thirteen_m) * line_values['ISWORKEDDAYSINCH']['sum']['total'] / line_values['ISWORKEDDAYS']['sum']['total']
salary_year = (line_values['ISSALARYDTAP']['sum']['total'] + thirteen_m) * year_days_ch / year_days
is_salary = salary_month + salary_year
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISSALARY', amount=is_salary, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
# ISDTSALARY and IS
# Monthly is model
if new_canton not in ["GE", "FR", "TI", "VS", "VD"]:
as_days = old_as_days
is_dt_salary = is_dt_salary_periodic / as_days * 30 + is_dt_salary_aperiodic
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARY', amount=is_dt_salary, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
min_is, rate = self._find_rate(f"{new_canton}-{new_code}-{new_municipality}", is_dt_salary)
is_amount = max(is_salary * rate / 100, min_is)
total_compensation -= is_amount
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='IS', amount=is_amount, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
# Yearly IS Model
else:
total_is_dt_periodic = log_lines['ISDTSALARYPERIODIC']
total_is_dt_aperiodic = log_lines['ISDTSALARYAPERIODIC']
total_as_days = log_lines['ASDAYS']
yearly_is_dt_salary = total_is_dt_periodic / total_as_days * 360 + total_is_dt_aperiodic
is_dt_salary = yearly_is_dt_salary / 12.0
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='ISDTSALARY', amount=is_dt_salary, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
is_log_lines = (previous_payslips + payslip_to_reverse)._get_is_log_lines()[new_canton]
code_is_salary, code_total_is = is_log_lines[new_code]['ISSALARY'], is_log_lines[new_code]['IS']
min_is, rate = self._find_rate(f"{new_canton}-{new_code}-{new_municipality}", is_dt_salary)
is_amount = code_is_salary * rate / 100 - code_total_is
if is_amount >= 0:
is_amount = max(min_is, is_amount)
self._log_is_line(is_canton=new_canton, is_code=new_code, municipality=new_municipality, code='IS', amount=is_amount, corrected_payslip_id=payslip_to_reverse.id, is_correction=True, correction_type='new')
total_compensation -= is_amount
return total_compensation
def _find_rate(self, is_code, x, date_from=False):
self.ensure_one()
if not date_from:
reference_date = self.date_from
else:
reference_date = date_from
if is_code[3:5] in ['HE', 'ME', 'NO', 'SF']:
canton, tax_code, _municipality = is_code.split("-")
category_code = tax_code[0:2]
church_tax = tax_code[2]
parameter_code = f"l10n_ch_withholding_tax_rates_{canton}_{category_code}_{church_tax}"
else:
canton, (tax_scale, child_count, church_tax), _municipality = is_code.split("-")
parameter_code = f"l10n_ch_withholding_tax_rates_{canton}_{church_tax}_{tax_scale}_{child_count}"
rates = self._rule_parameter(parameter_code, reference_date)
x = float_round(x, precision_rounding=1, rounding_method='DOWN')
for low, high, min_amount, rate in rates:
if low <= x <= high:
return min_amount, rate
return 0, 0
def _log_is_line(self, is_canton, is_code, municipality, code, amount, corrected_payslip_id=False, is_correction=False, correction_type=False, is_correction_id=False):
self.ensure_one()
if code in ['ASDAYS', 'ISWORKEDDAYSINCH', 'ISWORKEDDAYS']:
total_is = amount
else:
total_is = float_round(amount, precision_rounding=0.05, rounding_method="HALF-UP")
if total_is or code in ['ISDTSALARY', 'ISSALARY', 'IS']:
self.env['hr.payslip.is.log.line'].create({
'source_tax_canton': is_canton,
'source_tax_municipality': municipality,
'is_code': is_code,
'code': code,
'amount': total_is,
'payslip_id': self.id,
'is_correction': is_correction,
'corrected_slip_id': corrected_payslip_id,
'correction_type': correction_type,
'is_correction_id': is_correction_id,
})
def _get_is_log_lines(self, compute_total=False):
result = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
if not self:
return result
self.env.flush_all()
self.env.cr.execute("""
SELECT
pl.source_tax_canton,
pl.is_code,
pl.code,
SUM(pl.amount) as total
FROM hr_payslip_is_log_line pl
JOIN hr_payslip p
ON p.id IN %s
AND ((pl.corrected_slip_id = p.id AND pl.is_correction IS TRUE) OR (pl.payslip_id = p.id AND pl.is_correction IS NOT TRUE))
GROUP BY pl.source_tax_canton, pl.is_code, pl.code
""", (tuple(self.ids),))
request_rows = self.env.cr.dictfetchall()
result = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
for row in request_rows:
canton = row['source_tax_canton']
is_code = row['is_code']
code = row['code']
total = row['total']
result[canton][is_code].update({
code: total
})
if compute_total:
result["total"][canton][code] += total
return result
def action_refresh_from_work_entries(self):
swiss_slips = self.filtered(lambda p: p.struct_id.code == "CHMONTHLYELM" and p.state == 'draft')
if not swiss_slips:
return super().action_refresh_from_work_entries()
swiss_slips.mapped('l10n_ch_swiss_wage_ids').unlink()
swiss_slips.mapped('input_line_ids').unlink()
swiss_slips._compute_l10n_ch_monthly_snapshot()
swiss_slips._compute_l10n_ch_occupation()
swiss_slips._compute_l10n_ch_swiss_wage_ids()
swiss_slips._compute_input_line_ids()
swiss_slips._compute_l10n_ch_location_unit_id()
swiss_slips._compute_l10n_ch_social_insurance_id()
swiss_slips._compute_l10n_ch_lpp_insurance_id()
swiss_slips._compute_l10n_ch_laa_group()
swiss_slips._compute_laa_solution_number()
swiss_slips._compute_l10n_ch_additional_accident_insurance_line_ids()
swiss_slips._compute_l10n_ch_sickness_insurance_line_ids()
swiss_slips._compute_l10n_ch_pay_13th_month()
swiss_slips._compute_l10n_ch_avs_status()
swiss_slips._compute_l10n_ch_is_code()
swiss_slips._compute_l10n_ch_is_model()
swiss_slips._compute_l10n_ch_lpp_not_insured()
swiss_slips._compute_l10n_ch_compensation_fund_id()
swiss_slips._compute_issues()
swiss_slips.compute_sheet()
super(HrPayslip, self - swiss_slips).action_refresh_from_work_entries()
def action_open_source_tax_corrections(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Source-Tax Correction'),
'res_model': 'hr.employee.is.line',
'view_mode': 'form',
'res_id': self.l10n_ch_is_correction.id,
}
def action_absence_swiss_employee_from_payslip(self):
self.ensure_one()
return self.employee_id.action_absence_swiss_employee()
def _get_data_files_to_update(self):
# Note: file order should be maintained
return super()._get_data_files_to_update() + [(
'l10n_ch_hr_payroll', [
'data/hr_contract_type_data.xml',
'data/hr_payroll_structure_type_data.xml',
'data/hr_payroll_structure_data.xml',
'data/hr_payslip_input_type_data.xml',
'data/hr_rule_parameters_data.xml',
'data/hr_salary_rule_category_data.xml',
'data/hr_salary_rule_data.xml',
'data/hr_swiss_leave_types.xml',
'data/hr_payroll_dashboard_warning_data.xml',
])]
@api.depends('date_from', 'date_to', 'struct_id')
def _compute_warning_message(self):
swiss_slips = self.filtered(lambda p: p.struct_id.code == "CHMONTHLYELM")
super(HrPayslip, self - swiss_slips)._compute_warning_message()
@api.depends('employee_id', 'date_from', 'date_to', 'version_id.date_version')
def _compute_l10n_ch_occupation(self):
swiss_payslips = self.filtered(lambda p: p.struct_id.code == "CHMONTHLYELM")
if not swiss_payslips:
return
occupation_by_employee = dict(self.env['l10n.ch.occupation']._read_group(
domain=[],
groupby=['employee_id'],
aggregates=['id:recordset'],
))
for payslip in swiss_payslips:
payslip_date_from = payslip.date_from
payslip_date_to = payslip.date_to
employee_occupations = occupation_by_employee.get(payslip.employee_id, self.env['l10n.ch.occupation'])
matching_occupation = self.env['l10n.ch.occupation']
if employee_occupations:
potential_occupations = employee_occupations.filtered(
lambda occ: occ.date_start <= payslip_date_to and (occ.date_end is False or occ.date_end >= payslip_date_from)
)
# Shortcut computation for simple cases
if len(potential_occupations) == 1:
matching_occupation = potential_occupations
elif potential_occupations:
# Pick the occupation with start date closest to the current version date
current_version_validity = payslip.version_id.date_version
matching_occupation = min(
potential_occupations,
key=lambda occ: abs((occ.date_start - current_version_validity).days)
)
else:
# If no overlap, pick the most recent occupation before the payslip period
previous_occupations = employee_occupations.filtered(lambda occ: occ.date_start <= payslip_date_to)
if previous_occupations:
matching_occupation = max(previous_occupations, key=lambda occ: occ.date_start)
payslip.l10n_ch_entry = matching_occupation.date_start
payslip.l10n_ch_withdrawal = matching_occupation.date_end
def _l10n_ch_get_occupation_days(self, date_from=None, date_to=None):
self.ensure_one()
payslip = self
start = self.l10n_ch_entry
finish = self.l10n_ch_withdrawal
if not (start or finish) and not (date_from or date_to):
return 0
if date_from is not None:
start = date_from
if date_to is not None:
finish = date_to
if start == finish:
return 1
if start == self.date_to:
return 1
if finish and finish == self.date_from:
return 1
if start > payslip.date_from and finish and finish < payslip.date_to:
return (monthrange(payslip.date_from.year, payslip.date_from.month)[1] - ((start - payslip.date_from).days + 1) - ((payslip.date_to - finish).days)) + 1
if start > payslip.date_from:
return 31 - start.day
if finish and finish < payslip.date_to and not finish < payslip.date_from:
return finish.day
if finish and finish < payslip.date_from:
return 0
return 30
def action_adjust_payslip(self):
self.ensure_one()
if self.struct_id.code == "CHMONTHLYELM":
raise ValidationError(self.env._("This feature is not available for payslips in Switzerland. If you wish to correct amounts please cancel the payslip or report corrections to the next month."))
else:
return super().action_adjust_payslip()
@api.model
def _get_dashboard_warnings_domain(self):
res = super()._get_dashboard_warnings_domain()
if self.env.company.country_id.code == "CH":
res = Domain.AND([
res,
Domain('country_id.code', '!=', False),
])
return res
def _get_third_party_payment_wage_types(self):
return {
'WT_2000',
'WT_2005',
'WT_2010',
'WT_2020',
'WT_2021',
'WT_2022',
'WT_2025',
'WT_2026',
'WT_2027',
'WT_2030',
'WT_2031',
'WT_2032',
'WT_2035',
"WT_2040",
}