270 lines
18 KiB
Python
270 lines
18 KiB
Python
# -*- coding:utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import ValidationError, UserError
|
|
|
|
import re
|
|
import uuid
|
|
|
|
|
|
class HrEmployee(models.Model):
|
|
_inherit = 'hr.employee'
|
|
|
|
l10n_ch_sv_as_number = fields.Char(
|
|
string="Social Insurance N°",
|
|
groups="hr.group_hr_user",
|
|
help="Thirteen-digit AS number assigned by the Central Compensation Office (CdC)", tracking=True)
|
|
|
|
l10n_ch_children = fields.One2many('l10n.ch.hr.employee.children', 'employee_id', groups="hr_payroll.group_hr_payroll_user")
|
|
certificate = fields.Selection(ondelete={
|
|
'universityBachelor': 'set default',
|
|
'universityMaster': 'set default',
|
|
'higherEducationMaster': 'set default',
|
|
'higherEducationBachelor': 'set default',
|
|
'higherVocEducation': 'set default',
|
|
'higherVocEducationMaster': 'set default',
|
|
'higherVocEducationBachelor': 'set default',
|
|
'teacherCertificate': 'set default',
|
|
'universityEntranceCertificate': 'set default',
|
|
'vocEducationCompl': 'set default',
|
|
'enterpriseEducation': 'set default',
|
|
'mandatorySchoolOnly': 'set default',
|
|
'doctorate': 'set default'
|
|
}, default='mandatorySchoolOnly')
|
|
|
|
l10n_ch_legal_first_name = fields.Char(string="First Name", compute="_compute_l10n_ch_legal_name", store=True, readonly=False, tracking=True, groups="hr.group_hr_user")
|
|
l10n_ch_legal_last_name = fields.Char(string="Last Name", compute="_compute_l10n_ch_legal_name", store=True, readonly=False, tracking=True, groups="hr.group_hr_user")
|
|
l10n_ch_is_mutations = fields.One2many('l10n.ch.is.mutation', 'employee_id', groups="hr.group_hr_user")
|
|
l10n_ch_salary_certificate_profiles = fields.One2many("l10n.ch.salary.certificate.profile", "employee_id", groups="hr.group_hr_user")
|
|
|
|
l10n_ch_tax_scale_type = fields.Selection(readonly=False, related="version_id.l10n_ch_tax_scale_type", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_pre_defined_tax_scale = fields.Selection(readonly=False, related="version_id.l10n_ch_pre_defined_tax_scale", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_open_tax_scale = fields.Char(readonly=False, related="version_id.l10n_ch_open_tax_scale", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_tax_specially_approved = fields.Boolean(readonly=False, related="version_id.l10n_ch_tax_specially_approved", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_tax_code = fields.Char(readonly=True, related="version_id.l10n_ch_tax_code", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_source_tax_canton = fields.Char(readonly=True, related="version_id.l10n_ch_source_tax_canton", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_source_tax_municipality = fields.Char(readonly=True, related="version_id.l10n_ch_source_tax_municipality", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_other_employment = fields.Boolean(readonly=False, related="version_id.l10n_ch_other_employment", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_total_activity_type = fields.Selection(readonly=False, related="version_id.l10n_ch_total_activity_type", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_other_activity_percentage = fields.Float(readonly=False, related="version_id.l10n_ch_other_activity_percentage", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_other_activity_gross = fields.Float(readonly=False, related="version_id.l10n_ch_other_activity_gross", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_tax_scale = fields.Selection(readonly=False, related="version_id.l10n_ch_tax_scale", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_religious_denomination = fields.Selection(readonly=False, related="version_id.l10n_ch_religious_denomination", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_church_tax = fields.Boolean(readonly=False, related="version_id.l10n_ch_church_tax", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_job_type = fields.Selection(readonly=False, related="version_id.l10n_ch_job_type", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
irregular_working_time = fields.Boolean(readonly=False, related="version_id.irregular_working_time", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_weekly_hours = fields.Float(readonly=False, related="version_id.l10n_ch_weekly_hours", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_thirteen_month = fields.Boolean(readonly=False, related="version_id.l10n_ch_thirteen_month", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_social_insurance_id = fields.Many2one(readonly=False, related="version_id.l10n_ch_social_insurance_id", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_lpp_insurance_id = fields.Many2one(readonly=False, related="version_id.l10n_ch_lpp_insurance_id", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_accident_insurance_line_id = fields.Many2one(readonly=False, related="version_id.l10n_ch_accident_insurance_line_id", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_additional_accident_insurance_line_ids = fields.Many2many(readonly=False, related="version_id.l10n_ch_additional_accident_insurance_line_ids", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_sickness_insurance_line_ids = fields.Many2many(readonly=False, related="version_id.l10n_ch_sickness_insurance_line_ids", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_compensation_fund_id = fields.Many2one(readonly=False, related="version_id.l10n_ch_compensation_fund_id", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_lesson_wage = fields.Float(readonly=False, related="version_id.l10n_ch_lesson_wage", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_contractual_13th_month_rate = fields.Float(readonly=False, related="version_id.l10n_ch_contractual_13th_month_rate", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_location_unit_id = fields.Many2one(readonly=False, related="version_id.l10n_ch_location_unit_id", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_avs_status = fields.Selection(readonly=False, related="version_id.l10n_ch_avs_status", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_yearly_holidays = fields.Integer(readonly=False, related="version_id.l10n_ch_yearly_holidays", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_yearly_paid_public_holidays = fields.Integer(readonly=False, related="version_id.l10n_ch_yearly_paid_public_holidays", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_lpp_not_insured = fields.Boolean(readonly=False, related="version_id.l10n_ch_lpp_not_insured", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_current_occupation_rate = fields.Float(readonly=False, related="version_id.l10n_ch_current_occupation_rate", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_other_employers_occupation_rate = fields.Float(readonly=True, related="version_id.l10n_ch_other_employers_occupation_rate", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_total_occupation_rate = fields.Float(readonly=True, related="version_id.l10n_ch_total_occupation_rate", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_contractual_holidays_rate = fields.Float(readonly=False, related="version_id.l10n_ch_contractual_holidays_rate", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_contractual_public_holidays_rate = fields.Float(readonly=False, related="version_id.l10n_ch_contractual_public_holidays_rate", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_contractual_vacation_pay = fields.Boolean(readonly=False, related="version_id.l10n_ch_contractual_vacation_pay", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_contractual_annual_wage = fields.Monetary(readonly=False, related="version_id.l10n_ch_contractual_annual_wage", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_contract_wage_ids = fields.One2many(readonly=False, related="version_id.l10n_ch_contract_wage_ids", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
one_time_wage_count = fields.Integer(readonly=True, related="version_id.one_time_wage_count", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_has_monthly = fields.Boolean(readonly=False, related="version_id.l10n_ch_has_monthly", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_has_hourly = fields.Boolean(readonly=False, related="version_id.l10n_ch_has_hourly", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
l10n_ch_has_lesson = fields.Boolean(readonly=False, related="version_id.l10n_ch_has_lesson", inherited=True, groups="hr_payroll.group_hr_payroll_user")
|
|
registration_number = fields.Char(default=lambda self: str(uuid.uuid4().hex))
|
|
|
|
@api.constrains('birthday')
|
|
def _check_birthday(self):
|
|
today = fields.Datetime.now().date()
|
|
for employee in self:
|
|
if employee.birthday and employee.birthday > today:
|
|
raise ValidationError(_("Employee's Birthday cannot be greater than today."))
|
|
|
|
@api.onchange('private_country_id')
|
|
def _onchange_private_country_id(self):
|
|
self.version_id._onchange_private_country_id()
|
|
|
|
@api.onchange('l10n_ch_has_monthly')
|
|
def _onchange_l10n_ch_has_monthly(self):
|
|
self.version_id._onchange_l10n_ch_has_monthly()
|
|
|
|
@api.onchange('l10n_ch_has_hourly')
|
|
def _onchange_l10n_ch_has_hourly(self):
|
|
self.version_id._onchange_l10n_ch_has_hourly()
|
|
|
|
@api.onchange('l10n_ch_has_lesson')
|
|
def _onchange_l10n_ch_has_lesson(self):
|
|
self.version_id._onchange_l10n_ch_has_lesson()
|
|
|
|
def write(self, vals):
|
|
vals = super().write(vals)
|
|
# Recompute open payslips automatically on each update since almost all fields cause a change in computation
|
|
pending_computation_slips = self.sudo().slip_ids.filtered(lambda p: p.state == 'draft' and p.struct_id.code == "CHMONTHLYELM")
|
|
if pending_computation_slips:
|
|
earliest_payslip_date = min(pending_computation_slips.mapped('date_from'))
|
|
self.with_context(l10n_ch_reference_date=earliest_payslip_date)._create_or_update_snapshot()
|
|
pending_computation_slips.action_refresh_from_work_entries()
|
|
else:
|
|
self._create_or_update_snapshot()
|
|
return vals
|
|
|
|
def _get_certificate_selection(self):
|
|
if self.env.company.country_id.code != "CH":
|
|
return super()._get_certificate_selection()
|
|
return super()._get_certificate_selection() + [
|
|
('universityBachelor', self.env._('Swiss: University College Bachelor (university, ETH)')),
|
|
('universityMaster', self.env._('Swiss: University College Master (university, ETH)')),
|
|
('higherEducationMaster', self.env._('Swiss: University of Applied Sciences Master')),
|
|
('higherEducationBachelor', self.env._('Swiss: University of Applied Sciences Bachelor')),
|
|
('higherVocEducation', self.env._('Swiss: Higher Vocational Education')),
|
|
('higherVocEducationMaster', self.env._('Swiss: Higher Vocational Education Master')),
|
|
('higherVocEducationBachelor', self.env._('Swiss: Higher Vocational Education Bachelor')),
|
|
('teacherCertificate', self.env._('Swiss: Teaching certificate at different levels')),
|
|
('universityEntranceCertificate', self.env._('Swiss: Matura')),
|
|
('vocEducationCompl', self.env._('Swiss: Complete learning attested by a federal certificate of capacity (CFC)')),
|
|
('enterpriseEducation', self.env._('Swiss: In-company training only')),
|
|
('mandatorySchoolOnly', self.env._('Swiss: Compulsory schooling, without full vocational training')),
|
|
('doctorate', self.env._('Swiss: Doctorate, habilitation')),
|
|
]
|
|
|
|
@api.depends("name")
|
|
def _compute_l10n_ch_legal_name(self):
|
|
for employee in self:
|
|
if employee.name:
|
|
first_name = ' '.join(re.sub(r"\([^()]*\)", "", employee.name).strip().split()[:-1])
|
|
last_name = re.sub(r"\([^()]*\)", "", employee.name).strip().split()[-1]
|
|
if not employee.l10n_ch_legal_last_name:
|
|
employee.l10n_ch_legal_last_name = last_name
|
|
if not employee.l10n_ch_legal_first_name:
|
|
employee.l10n_ch_legal_first_name = first_name
|
|
|
|
@api.depends('l10n_ch_legal_first_name', 'l10n_ch_legal_last_name')
|
|
def _compute_legal_name(self):
|
|
ch_employees = self.filtered(lambda e: e.company_id.country_code == 'CH')
|
|
for employee in ch_employees:
|
|
if employee.l10n_ch_legal_first_name and employee.l10n_ch_legal_last_name:
|
|
employee.legal_name = f'{employee.l10n_ch_legal_first_name} {employee.l10n_ch_legal_last_name}'
|
|
else:
|
|
employee.legal_name = employee.name
|
|
super(HrEmployee, self - ch_employees)._compute_legal_name()
|
|
|
|
@api.model
|
|
def _create_or_update_snapshot(self):
|
|
|
|
swiss_employees = self.filtered(lambda e: e.company_id.country_id.code == "CH")
|
|
if not swiss_employees:
|
|
return
|
|
|
|
self.env.flush_all()
|
|
|
|
ref_date = self.env.context.get('l10n_ch_reference_date') or fields.Date.context_today(self)
|
|
|
|
month = ref_date.month
|
|
year = ref_date.year
|
|
|
|
existing_snapshots = self.sudo().env["l10n.ch.employee.yearly.values"].search([
|
|
('year', '=', year),
|
|
('employee_id', 'in', swiss_employees.ids)
|
|
])
|
|
snapshots_to_update = existing_snapshots.mapped('employee_id')
|
|
snapshots_to_create = swiss_employees - snapshots_to_update
|
|
vals = []
|
|
for employee in snapshots_to_create:
|
|
vals.append({
|
|
'employee_id': employee.id,
|
|
'year': year
|
|
})
|
|
|
|
if vals:
|
|
existing_snapshots += self.sudo().env['l10n.ch.employee.yearly.values'].create(vals)
|
|
|
|
existing_snapshots += self.sudo().env["l10n.ch.employee.yearly.values"].search([
|
|
('year', '>', year),
|
|
('employee_id', 'in', self.ids)
|
|
])
|
|
unlock_pay_period = self.env.context.get('unlock_pay_period')
|
|
|
|
# Mutation insensitive informations, these have to be updated even if the payroll month is closed
|
|
monthly_persons_to_update = existing_snapshots.monthly_value_ids.filtered(lambda s: not s.payroll_month_closed or (s.month >= month and s.year >= year)).sorted(lambda s: (s.year, s.month))
|
|
self.env.add_to_compute(self.env['l10n.ch.employee.monthly.values']._fields['person'], monthly_persons_to_update)
|
|
monthly_persons_to_update._recompute_recordset(['person'])
|
|
|
|
# Mutation sensitive informations, these should not be recomputed once payroll month is closed
|
|
monthly_values_to_update = existing_snapshots.monthly_value_ids.filtered(lambda s: not s.payroll_month_closed or (s.month >= month and s.year >= year and unlock_pay_period)).sorted(lambda s: (s.year, s.month))
|
|
|
|
if self.env.context.get('update_salaries'):
|
|
self.env.add_to_compute(self.env['l10n.ch.employee.monthly.values']._fields['bvg_lpp_annual_basis'], monthly_values_to_update)
|
|
monthly_values_to_update._recompute_recordset(['bvg_lpp_annual_basis'])
|
|
|
|
self.env.add_to_compute(self.env['l10n.ch.employee.monthly.values']._fields['employee_meta_data'], monthly_values_to_update)
|
|
self.env.add_to_compute(self.env['l10n.ch.employee.monthly.values']._fields['additional_particular'], monthly_values_to_update)
|
|
monthly_values_to_update._recompute_recordset(['employee_meta_data', 'additional_particular'])
|
|
|
|
self.env.add_to_compute(self.env['l10n.ch.employee.monthly.values']._fields['lpp_mutations'], monthly_values_to_update)
|
|
self.env.add_to_compute(self.env['l10n.ch.employee.monthly.values']._fields['is_mutations'], monthly_values_to_update)
|
|
monthly_values_to_update._recompute_recordset(['lpp_mutations', 'is_mutations'])
|
|
|
|
self.env.add_to_compute(self.env['l10n.ch.employee.monthly.values']._fields['monthly_statistics'], monthly_values_to_update)
|
|
monthly_values_to_update._recompute_recordset(['monthly_statistics'])
|
|
|
|
existing_snapshots._toggle_pay_period_lock()
|
|
|
|
def action_absence_swiss_employee(self):
|
|
return {
|
|
'name': _('Absences'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'hr.leave',
|
|
'views': [[self.env.ref('l10n_ch_hr_payroll.l10n_ch_hr_leave_employee_view_dashboard').id, 'calendar']],
|
|
'domain': [('employee_id', 'in', self.ids)],
|
|
'context': {
|
|
'employee_id': self.ids,
|
|
},
|
|
}
|
|
|
|
def action_view_wages(self):
|
|
self.ensure_one()
|
|
if self.version_id:
|
|
return self.version_id.action_view_wages()
|
|
else:
|
|
raise UserError(_("Oops, this employee has no contract yet."))
|
|
|
|
@api.model
|
|
def _validate_sv_as_number(self, sv_as_number):
|
|
pattern = r"^\d{3}\.\d{4}\.\d{4}\.\d{2}$"
|
|
if not re.match(pattern, sv_as_number):
|
|
raise UserError(
|
|
_('The SV-AS number should be a thirteen-digit number, dot-separated (eg: 756.1848.4786.64)'))
|
|
|
|
sv_as_number = sv_as_number.replace('.', '')
|
|
first_12_digits = sv_as_number[:12]
|
|
weights = [1, 3] * 6 # Alternating pattern
|
|
weighted_sum = sum(int(digit) * weight for digit, weight in zip(first_12_digits, weights))
|
|
nearest_multiple_of_10 = (weighted_sum + 9) // 10 * 10
|
|
check_digit = nearest_multiple_of_10 - weighted_sum
|
|
|
|
# Compare with the 13th digit of the number
|
|
if check_digit != int(sv_as_number[-1]):
|
|
raise UserError(
|
|
_('Incorrect EAN13 Check-sum for this SV-AS Number'))
|
|
|
|
@api.constrains('l10n_ch_sv_as_number')
|
|
def _check_l10n_ch_sv_as_number(self):
|
|
"""
|
|
SV-AS number is encoded using EAN13 Standard Checksum control
|
|
"""
|
|
for employee in self:
|
|
if not employee.l10n_ch_sv_as_number:
|
|
continue
|
|
self._validate_sv_as_number(employee.l10n_ch_sv_as_number)
|