Salary structures and rules¶
The computation of a payslip is entirely defined by data: a salary structure
(hr.payroll.structure) is an ordered list of salary rules (hr.salary.rule), and each rule
computes one line of the payslip. Rules credit their totals to categories
(hr.salary.rule.category), whose running totals the next rules read (e.g. the net salary is
the sum of the basic, allowance and deduction categories). Structures are grouped by structure
type (hr.payroll.structure.type), which is what an employee version points to.
![digraph structures {
rankdir=LR;
node [shape=box, style=rounded, fontname="sans-serif", fontsize=10];
edge [fontname="sans-serif", fontsize=9];
version [label="hr.version"];
stype [label="hr.payroll.structure.type\n(e.g. Employee)"];
struct [label="hr.payroll.structure\n(e.g. Regular Pay, 13th Month)"];
rule [label="hr.salary.rule\n(BASIC, GROSS, TAX, NET...)"];
categ [label="hr.salary.rule.category\n(BASIC, ALW, DED, NET...)"];
section [label="hr.salary.rule.section"];
version -> stype [label="structure_type_id"];
version -> struct [label="structure_id\n(= default_struct_id)"];
stype -> struct [label="struct_ids,\ndefault_struct_id"];
struct -> rule [label="rule_ids"];
rule -> categ [label="category_ids"];
rule -> section [label="input_section"];
categ -> categ [label="parent_id"];
}](../../../../_images/graphviz-5bd1e409dadb264cc6a81b291c1fd09b4d071668.png)
Localizations ship their structures, rules and categories as module data (see Writing a payroll localization); users can adapt them or create their own.
Structure types¶
The model hr.payroll.structure.type is defined in hr (name, country, default working
schedule, see Organization and configuration), so that employee versions can reference it
without the payroll. hr_payroll adds the payroll configuration:
struct_ids,default_struct_idThe structures of the type, and the regular pay structure among them. A payslip is regular (
hr.payslip.is_regular) when its structure is thedefault_struct_idof its type; other structures are used for off-cycle payslips (13th month, double holidays, final pay…). New payslips take thedefault_struct_idof the version’s structure type.wage_typemonthly(Fixed Wage) orhourly. Copied tohr.version.wage_typewhen the structure type of a version changes; see Payroll data of versions.default_schedule_payThe pay frequency (
monthly,weekly,bi-weekly, …), copied tohr.version.schedule_pay. The available frequencies come from_get_selection_schedule_pay(), which localizations override to restrict them (e.g.l10n_bd_hr_payrollonly keepsmonthlyfor Bangladeshi companies).default_work_entry_type_idThe work entry type of regular attendances. By default, the
002.00type of the company country, or the generic attendance type. See Work entries.salary_benefits_idsThe salary benefits taken into account in the yearly cost of the versions.
Structures¶
A structure (hr.payroll.structure) has a required type_id, a country_id and the list of
its rules (rule_ids). Rules are many2many: a rule can belong to several structures, and its
code must be unique within each of its structures.
Other fields configure the payslips using the structure:
report_id: the QWeb PDF report of the payslips;payslip_name: the name of the payslips (e.g. End of Year Bonus), Salary Slip if empty;use_worked_day_lines: if unset, the payslips have no worked days lines (bonus structures);categoriesis then not pre-filled (see below);ytd_computation: displays the year-to-date amounts on the payslips;employee_type_ids: the employee types the structure is meant for;schedule_pay: related to the pay frequency of the structure type.
Important
A structure created without rule_ids gets copies of the generic rules of the archived
hr_payroll.default_structure (basic salary, gross, net, attachments, deductions,
reimbursements), see _get_default_rule_ids().
This is convenient in the user interface but not in module data, where the rules are declared
separately: data files always set <field name="rule_ids" eval="[]"/> on structures.
version_properties_definition holds the definition of the payroll properties of the versions
using the structure (hr.version.payroll_properties): one float property per rule flagged
input_usage_employee, grouped by section. It is rebuilt by
_sync_properties_to_definition() each time a rule
is created, modified or deleted; never write it directly. See
Rules as inputs.
Categories¶
A category (hr.salary.rule.category) is a named amount the rules contribute to: its code is
the key of the categories dictionary available in the rules. During the computation of a
payslip:
categoriesis pre-filled from the worked days: the amount of each worked days line is added to the categories of its work entry type (hr.work.entry.type.category_ids) and to their ancestors. This is how the attendance and paid time off amounts reach theBASICcategory without any rule.Each computed rule adds its total to its categories (
category_ids, a rule can have several) and to their ancestors.
The parent of a category (parent_id) is therefore not only for reporting, despite its help
text: a rule in TAXES (child of DED) also increases categories['DED'].
hr_payroll defines the base categories, without parents: BASIC, ALW (allowances), GROSS,
DED (deductions), NET, COMP (company contributions) and EXTRA_HOURS. Localizations add
their own categories, usually as children of these ones (country_id set to their country).
The usual conventions are:
the basic salary rule is
result = categories['BASIC'], without category (it would double the amount already pre-filled from the worked days);deductions are negative amounts, so that the net salary is a plain sum:
result = categories['BASIC'] + categories['ALW'] + categories['DED'];employer costs (
COMP) are not part of the net.
Example
On a payslip of an employee working the whole month for a wage of 35000, before any rule is computed:
localdict = payslip._get_localdict()
dict(localdict['categories'])
# {'BASIC': 35000.0}, from the worked days line of the attendance type 002.00
Categories also carry the premium pay configuration (is_premium_pay,
premium_amount_per_hour, premium_amount_per_day, premium_percentage_hourly_rate), and can
be offered as options on time off of some work entry types (optional_on_work_entry_type_ids):
the hours carrying an option are summed by category code in the category_options variable of
the rules.
Salary rules¶
A rule (hr.salary.rule) has a name (the default label of the payslip line), a code (the
variable name of its total in the next rules: use a valid Python identifier), its structures
(struct_ids, required), its categories (category_ids) and a country_id. Then:
- Order of computation
Rules are computed by increasing
sequence. Rules with a higherround_of_computationare computed later: all the payslips computed together (e.g. a pay run) run round 0, then round 1, etc. This is needed when a rule depends on the results of other payslips of the same batch (see Payslips).- Condition and amount
condition_selectandamount_select, with the related fields; see Condition and amount of a rule.- Display
appears_on_payslip(always,never,non_zero) filters the lines on the PDF only: the line is always created and always counts in its categories.title,bold,italic,underline,indented,space_above,color,hide_amountandnotecontrol the rendering;display_in_pdf_extra_infomoves the amount to an Additional Information section.- Explanation
explanation_templateis a translatablestr.formattemplate filled with theexplanation_infodict set by the rule code, stored on the line (explanation).- Accounting and costs
partner_idis the third party paid by the rule (withhr_payroll_account, which also adds the accounts).appears_on_employee_cost_dashboardincludes the line in the employer cost.
Copying a rule appends _COPY to its code, to keep codes unique in its structures.
Condition and amount of a rule¶
For each payslip, the engine resets the outputs of the rule (result = None, result_qty = 1.0,
result_rate = 100, result_name = False, explanation_info = {}), evaluates the condition
(_satisfy_condition()), and if it is satisfied, computes the
amount, quantity and rate of the line (_compute_rule()). The
total of the line is:
total = amount * quantity * rate / 100
(hr.payslip._get_payslip_line_total, which localizations override to round the totals).
Value |
The rule is computed if… |
|---|---|
|
always (Always Present). |
|
the code of |
|
the payslip matches the domain of |
|
the payslip has an input line with the code of the rule and a non-zero amount (Input
Value). Choosing it also sets |
Value |
amount, quantity and rate of the line |
|---|---|
|
|
|
the evaluation of |
|
the code of |
|
the amount of the input line with the code of the rule (0 if there is none); 1; 100. |
Python code of a rule¶
condition_python, amount_python_compute, quantity and amount_percentage_base are
evaluated with safe_eval() in the evaluation context of the
payslip (the localdict). A python condition or amount code is a sequence of statements and
sets these variables:
resultThe amount of the line (python amount), or the value of the condition (python condition).
result_qtyThe quantity of the line (default
1.0).result_rateThe rate of the line, in percent (default
100).result_nameThe label of the line, instead of the name of the rule.
explanation_infoThe values used to format
explanation_template.
The main variables of the context are listed below. The full description of the localdict, and how to add keys to it, is in Payslips.
Variable |
Content |
|---|---|
|
The |
|
|
|
|
|
The total of a rule already computed, e.g. |
|
|
|
|
|
|
|
The generated work entry values (dicts) of the payslip period. |
helpers |
|
Methods of the payslip are commonly used in rules: payslip._rule_parameter(code) (see
Rule parameters), payslip._sum(code, from_date, to_date) (totals of
a rule on the previous validated payslips of the employee), payslip._sum_category(...),
payslip._sum_worked_days(...).
Example
Typical rules (real ones from l10n_bd_hr_payroll/data/hr_salary_rule_data.xml):
# BASIC, no category: the amount of the worked days
result = categories['BASIC']
# ATTACH_SALARY, condition (python)
result = 'ATTACH_SALARY' in inputs
# ATTACH_SALARY, amount (python): a deduction is negative
result = -inputs['ATTACH_SALARY'].amount
result_name = inputs['ATTACH_SALARY'].name
# TAXES (shortened): progressive brackets from a rule parameter
yearly_taxable_amount = result_rules['TAXABLE_AMOUNT']['total'] * 12
income_tax = 0
for amount, rate in payslip._rule_parameter('l10n_bd_yearly_tax_rates'):
taxable_in_current = min(yearly_taxable_amount, amount)
yearly_taxable_amount -= taxable_in_current
income_tax += taxable_in_current * rate
result = -income_tax / 12
# NET
result = categories['BASIC'] + categories['ALW'] + categories['DED']
Example
Adding rules to a structure: a meal allowance per worked day, using result_qty and
result_name, and a seniority bonus with an explanation.
from odoo.fields import Command
struct = env.ref('l10n_bd_hr_payroll.hr_payroll_structure_bd_employee_salary')
env['hr.salary.rule'].create([{
'name': 'Meal Allowance',
'code': 'MEAL',
'sequence': 50,
'struct_ids': [Command.link(struct.id)],
'category_ids': [Command.link(env.ref('hr_payroll.ALW').id)],
'condition_select': 'python',
'condition_python': "result = '002.00' in worked_days",
'amount_select': 'code',
'amount_python_compute': (
"days = sum(worked_days['002.00'].mapped('number_of_days'))\n"
"result = 150\n"
"result_qty = days\n"
"result_name = 'Meal Allowance (%s days)' % days\n"
),
}, {
'name': 'Seniority Bonus',
'code': 'SENIORITY',
'sequence': 6,
'struct_ids': [Command.link(struct.id)],
'category_ids': [Command.link(env.ref('hr_payroll.ALW').id)],
'condition_select': 'none',
'amount_select': 'code',
'amount_python_compute': (
"years = relativedelta(payslip.date_to, version.contract_date_start).years\n"
"result = BASIC * 0.01\n"
"result_qty = years\n"
"explanation_info = {'basic': BASIC, 'years': years}\n"
),
'explanation_template': "1% of {basic} x {years} years",
}])
payslip.compute_sheet()
payslip.line_ids.filtered(lambda l: l.code in ('MEAL', 'SENIORITY')).mapped(
lambda l: (l.code, l.name, l.amount, l.quantity, l.total, l.explanation))
# [('SENIORITY', 'Seniority Bonus', 350.0, 6.0, 2100.0, '1% of 35000.0 x 6 years'),
# ('MEAL', 'Meal Allowance (22.0 days)', 150.0, 22.0, 3300.0, False)]
And a percentage rule with a domain condition, which gives a line of amount -35000, rate 5 and total -1750 to the employees who are not disabled:
env['hr.salary.rule'].create({
'name': 'Pension Fund',
'code': 'PENSION',
'sequence': 150,
'struct_ids': [Command.link(struct.id)],
'category_ids': [Command.link(env.ref('hr_payroll.DED').id)],
'condition_select': 'domain',
'condition_domain': "[('employee_id.disabled', '=', False)]",
'amount_select': 'percentage',
'amount_percentage_base': "-categories['BASIC']",
'amount_percentage': 5.0,
'quantity': '1.0',
})
An error in the code of a rule raises a UserError giving the employee, the payslip, the rule
and the original error, e.g. KeyError('LEAVE110') for worked_days['LEAVE110'] on a payslip
without such worked days.
Writing robust rule code¶
worked_days[code]andinputs[code]raiseKeyErrorwhen the code is absent: test'X' in worked_daysfirst, or useinputs.get('X'). On the contrary,categoriesandresult_rulesreturn 0 for any code, so a misspelled code silently gives 0.worked_days[code]is a recordset that may contain several lines (several versions in the period, category options): usesum(worked_days[code].mapped('amount'))rather thanworked_days[code].amount.Several input lines with the same code are aggregated in
inputs[code](summedamount), and the rule is computed once per input line, producing one payslip line each. In that case, the rule code is not defined as a variable for the next rules: useresult_rules.Every variable assigned by a rule stays in the context for the next rules of the payslip. Do not rely on a variable set by another rule (it may not be computed), and initialize the variables you use: a variable assigned only in some branches keeps the value of a previous rule in the others.
A rule code is a variable only once the rule is computed: use
result_rules['CODE']['total']to read the total of a rule that may not apply.The order is given by
round_of_computation, thensequence, not by the order of the records: a rule must come after the rules it reads.
Rules as inputs¶
A rule can also define an input: a value entered by the user and read by the rule through
inputs[code]. Input values come from three sources, all stored as hr.payslip.input lines
pointing to the rule (salary_rule_id):
input_usage_employeeThe value is set on the employee version, as a payroll property named after the rule code (
hr.version.payroll_properties, see Payroll properties).input_name,input_suffix,input_default_valueandinput_sectiondefine the property.input_usage_payslip(Available on Payslip)The value is entered on the payslip, or comes from a salary attachment (payslip adjustment) of the employee, see Payslip adjustments.
input_selected_by_defaultNew payslips get an input line with
input_default_value, even if it is 0.
user_instructions explains to the user what to enter. A rule whose condition or amount is
property_input must have input_usage_employee or input_usage_payslip (otherwise it could
never receive a value), and its code may only contain letters, digits and underscores.
Sections (hr.salary.rule.section) group the employee inputs in the version form: each section
is a separator of the properties definition, ordered by sequence. A rule without section is
not part of the definition. hr_payroll.default_salary_rule_section (Inputs) is the default
section.
Customized rules and resets¶
Localization data is updated with the law, but users also adapt rules. To combine both, when a
user changes a field of the computation (_get_critical_fields():
code, categories, structures, sequence, condition and amount fields) on a rule created by the
system, the rule is flagged modified_by_user and its XML ids become noupdate: module updates
no longer overwrite it. action_reset_rule() reverts this and
reloads the payroll data files of the country. Rules created by the system cannot be deleted by
users, only archived.
A reset rewrites only the fields present in the data files. This is why the rules declared in
module data must explicitly set the fields relevant to their type
(_get_reset_required_explicit_fields()), and set
category_ids with a single [(5, 0, 0)] or [(6, 0, [...])] command: otherwise, a warning
is logged at install. See Writing a payroll localization.