217 lines
9.2 KiB
Python
217 lines
9.2 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from odoo import api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class HrPayrollStructure(models.Model):
|
|
_name = 'hr.payroll.structure'
|
|
_description = 'Salary Structure'
|
|
|
|
@api.model
|
|
def _get_default_report_id(self):
|
|
return self.env.ref('hr_payroll.action_report_payslip', False)
|
|
|
|
@api.model
|
|
def _get_default_rule_ids(self):
|
|
default_structure = self.env.ref('hr_payroll.default_structure', False)
|
|
if not default_structure or not default_structure.rule_ids:
|
|
return []
|
|
vals_list = [rule.copy_data(default={'name': rule.name})[0] for rule in default_structure.rule_ids]
|
|
return [(0, 0, vals) for vals in vals_list]
|
|
|
|
def _get_domain_report(self):
|
|
if self.env.company.country_code:
|
|
return [
|
|
('model', '=', 'hr.payslip'),
|
|
('report_type', '=', 'qweb-pdf'),
|
|
'|',
|
|
('report_name', 'ilike', 'l10n_' + self.env.company.country_code.lower()),
|
|
'&',
|
|
('report_name', 'ilike', 'hr_payroll'),
|
|
('report_name', 'not ilike', 'l10n')
|
|
]
|
|
else:
|
|
return [
|
|
('model', '=', 'hr.payslip'),
|
|
('report_type', '=', 'qweb-pdf'),
|
|
('report_name', 'ilike', 'hr_payroll'),
|
|
('report_name', 'not ilike', 'l10n')
|
|
]
|
|
|
|
name = fields.Char(required=True, translate=True)
|
|
code = fields.Char()
|
|
active = fields.Boolean(default=True)
|
|
type_id = fields.Many2one(
|
|
'hr.payroll.structure.type', required=True, index=True)
|
|
country_id = fields.Many2one(
|
|
'res.country',
|
|
string='Country',
|
|
default=lambda self: self.env.company.country_id,
|
|
domain=lambda self: [('id', 'in', self.env.companies.country_id.ids)]
|
|
)
|
|
note = fields.Html(string='Description')
|
|
rule_ids = fields.One2many(
|
|
'hr.salary.rule', 'struct_id', copy=True,
|
|
string='Salary Rules', default=_get_default_rule_ids)
|
|
report_id = fields.Many2one('ir.actions.report',
|
|
string="Template", domain=_get_domain_report, default=_get_default_report_id)
|
|
payslip_name = fields.Char(string="Payslip Name", translate=True,
|
|
help="Name to be set on a payslip. Example: 'End of the year bonus'. If not set, the default value is 'Salary Slip'")
|
|
hide_basic_on_pdf = fields.Boolean(help="Enable this option if you don't want to display the Basic Salary on the printed pdf.")
|
|
unpaid_work_entry_type_ids = fields.Many2many(
|
|
'hr.work.entry.type', 'hr_payroll_structure_hr_work_entry_type_rel')
|
|
use_worked_day_lines = fields.Boolean(default=True, help="Worked days won't be computed/displayed in payslips.")
|
|
schedule_pay = fields.Selection(related='type_id.default_schedule_pay')
|
|
input_line_type_ids = fields.Many2many('hr.payslip.input.type', string='Other Input Line')
|
|
ytd_computation = fields.Boolean(default=False, string='Year to Date Computation',
|
|
help="Adds a column in the payslip that shows the accumulated amount paid for different rules during the year")
|
|
|
|
version_properties_definition = fields.PropertiesDefinition("Version Properties Definition")
|
|
payslip_properties_definition = fields.PropertiesDefinition("Payslip Properties Definition")
|
|
|
|
def copy_data(self, default=None):
|
|
vals_list = super().copy_data(default=default)
|
|
return [dict(vals, name=self.env._("%s (copy)", structure.name)) for structure, vals in zip(self, vals_list)]
|
|
|
|
def _get_common_payroll_properties(self):
|
|
"""
|
|
Helper method to return the common salary inputs between employees and payslips
|
|
"""
|
|
payslip_properties = self.payslip_properties_definition or []
|
|
version_properties = self.version_properties_definition or []
|
|
|
|
payslip_payroll_keys = {
|
|
prop['name'] for prop in payslip_properties if not prop['name'].startswith('separator_')
|
|
}
|
|
version_payroll_keys = {
|
|
prop['name'] for prop in version_properties if not prop['name'].startswith('separator_')
|
|
}
|
|
|
|
# Return the intersection of the two sets
|
|
return list(payslip_payroll_keys & version_payroll_keys)
|
|
|
|
def _update_payroll_properties(self, rule_ids, res_model):
|
|
self.ensure_one()
|
|
payslip_properties_definition = self.payslip_properties_definition or []
|
|
version_properties_definition = self.version_properties_definition or []
|
|
|
|
def _sort_definition(definition):
|
|
groups = []
|
|
current_group = None
|
|
for prop in definition:
|
|
if prop.get("type") == "separator":
|
|
if current_group:
|
|
groups.append(current_group)
|
|
current_group = [prop, []]
|
|
elif current_group:
|
|
current_group[1].append(prop)
|
|
if current_group:
|
|
groups.append(current_group)
|
|
cat_ids = []
|
|
for g in groups:
|
|
sep = g[0]
|
|
if sep['name'].startswith('separator_'):
|
|
cat_id = int(sep['name'][10:])
|
|
cat_ids.append(cat_id)
|
|
categories = self.env['hr.salary.rule.section'].browse(cat_ids)
|
|
seq_map = {c.id: c.sequence for c in categories}
|
|
groups.sort(key=lambda g: seq_map.get(int(g[0]['name'][10:]), float('inf')))
|
|
new_def = []
|
|
for sep, props in groups:
|
|
new_def.append(sep)
|
|
new_def.extend(props)
|
|
return new_def
|
|
|
|
def _add_property_to_definition(definition, rule):
|
|
category = rule.input_section
|
|
if not category:
|
|
raise ValidationError(self.env._('You cannot add a Salary input without a section.'))
|
|
|
|
separator_name = f"separator_{category.id}"
|
|
property_name = str(rule.id)
|
|
existing_prop = [prop for prop in definition if prop.get("name") == property_name]
|
|
|
|
type = (
|
|
"monetary" if rule.input_unit == "monetary"
|
|
else "boolean" if rule.input_unit == "boolean"
|
|
else "float"
|
|
)
|
|
suffix_base = "% " if rule.input_unit == 'percentage' else ""
|
|
suffix = suffix_base + rule.input_suffix if rule.input_suffix else ""
|
|
|
|
if existing_prop:
|
|
existing_prop[0].update({
|
|
"string": rule.name,
|
|
"default": rule.input_default_value if type != 'boolean' else rule.input_default_boolean,
|
|
})
|
|
if suffix:
|
|
existing_prop[0].update({
|
|
'suffix': suffix
|
|
})
|
|
return
|
|
|
|
new_property = {
|
|
"name": property_name,
|
|
"type": type,
|
|
"string": rule.name,
|
|
"default": rule.input_default_value if type != 'boolean' else rule.input_default_boolean,
|
|
}
|
|
|
|
if suffix:
|
|
new_property['suffix'] = suffix
|
|
|
|
if type == 'monetary':
|
|
new_property['currency_field'] = 'currency_id'
|
|
|
|
sep_index = None
|
|
for i, prop in enumerate(definition):
|
|
if prop.get("type") == "separator" and prop.get("name") == separator_name:
|
|
sep_index = i
|
|
break
|
|
|
|
if sep_index is not None:
|
|
next_sep_index = len(definition)
|
|
for j in range(sep_index + 1, len(definition)):
|
|
if definition[j].get("type") == "separator":
|
|
next_sep_index = j
|
|
break
|
|
definition.insert(next_sep_index, new_property)
|
|
else:
|
|
new_separator = {
|
|
"name": separator_name,
|
|
"type": "separator",
|
|
"string": category.name,
|
|
"fold_by_default": False,
|
|
}
|
|
definition.append(new_separator)
|
|
definition.append(new_property)
|
|
|
|
dependent_rules = self.env['hr.salary.rule'].search([('dependent_input_id', 'in', rule_ids.ids)])
|
|
all_rules = rule_ids | dependent_rules
|
|
|
|
for rule in all_rules:
|
|
if res_model in ['hr.employee', 'hr.version']:
|
|
_add_property_to_definition(version_properties_definition, rule)
|
|
elif res_model == 'hr.payslip':
|
|
_add_property_to_definition(payslip_properties_definition, rule)
|
|
|
|
self.write({
|
|
'payslip_properties_definition': _sort_definition(payslip_properties_definition),
|
|
})
|
|
self.write({
|
|
'version_properties_definition': _sort_definition(version_properties_definition),
|
|
})
|
|
|
|
def action_get_structure_inputs(self):
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'list',
|
|
'view_id': self.env.ref("hr_payroll.hr_salary_rule_benefit_selector_list", False).id,
|
|
'res_model': 'hr.salary.rule',
|
|
'target': 'new',
|
|
'domain': [
|
|
('struct_id', '=', self.id),
|
|
('condition_select', '=', 'property_input'),
|
|
]
|
|
}
|