Payroll data of versions¶
There is no contract model in Odoo 20.0: the contractual data of an employee lives on its
versions (hr.version, see Versions and contracts). hr_payroll extends hr.version
with what the payroll needs (wage, pay frequency, salary structure, payroll properties, yearly
cost), and exposes most of these fields on hr.employee.
Fields on the employee and on the version¶
The payroll fields of the version are also fields of the employee, declared as
related='version_id.<field>', inherited=True (hr_payroll/models/hr_employee.py): wage,
hourly_wage, wage_type, schedule_pay, structure_type_id, structure_id,
payroll_properties, final_yearly_costs, work_time_rate, disabled... Reading
employee.wage reads the current version of the employee, and writing it writes the current
version. To read the data valid at another date (e.g. in a payslip), use the version itself:
payslip.version_id, or employee._get_version(date).
Most payroll fields are restricted to the payroll users (hr_payroll.group_hr_payroll_user).
Wage and pay frequency¶
wage_typemonthly(Fixed Wage: the wage of a pay period iswage) orhourly(the wage ishourly_wageper hour). Computed from the structure type when it changes, editable.wage,hourly_wageThe gross wage per pay period, and the hourly wage.
schedule_payThe pay frequency (
monthly,weekly,bi-weekly, ...), computed from thedefault_schedule_payof the structure type, editable. It gives the default period of new payslips.
Use the helpers rather than reading the fields directly, as their logic is overridden by localizations:
_get_contract_wage()The wage of the version, i.e. the value of the field returned by
_get_contract_wage_field():hourly_wagefor hourly versions,wageotherwise (localizations return other fields, e.g. a wage on payroll)._get_salary_costs_factor()The number of pay periods per year for
schedule_pay: 12 for monthly, 52 for weekly, 26 for bi-weekly, 52 times the days per week of the calendar for daily..._get_normalized_wage()The hourly wage:
hourly_wagefor hourly versions, elsewage * pay periods per year / 52 / hours per week.
Example
version = employee.version_id
version.wage_type, version.schedule_pay, version.wage
# ('monthly', 'monthly', 35000.0)
version._get_contract_wage(), version._get_contract_wage_field()
# (35000.0, 'wage')
version._get_salary_costs_factor(), version._get_normalized_wage()
# (12, 201.92307692307693) for a 40 hours/week calendar: 35000 * 12 / 52 / 40
Salary structure¶
structure_type_id (defined in hr) selects the kind of contract. structure_id is a stored
related field on structure_type_id.default_struct_id: the regular pay structure of the
version. It is not set per version: changing the default structure of a structure type changes
it on all its versions. See Salary structures and rules.
work_time_rate is the ratio between the weekly hours of the working schedule of the version
and those of its reference (full time) calendar, e.g. 0.8 for a 4/5 time.
Payroll properties¶
payroll_properties is a properties field whose definition is the
version_properties_definition of structure_id: one float property per salary rule of the
structure flagged input_usage_employee, named after the rule code (see
Rules as inputs). It holds the values specific to the employee
(number of dependents, a fixed allowance, a pension fund rate...). When a payslip of the regular
structure is created, these values become input lines of the payslip, read by the rules as
inputs['CODE'].amount.
Read and write the values with _get_property_input_value() (0.0 if
the structure has no such rule or the version no value) and
_set_property_input_value() (raises a ValidationError if the rule
is not an employee input).
Example
In l10n_bd_hr_payroll, DISABLEDDEDUCTION is an employee input (number of disabled
dependents) increasing the tax exemption:
from datetime import date
version._set_property_input_value('DISABLEDDEDUCTION', 2)
dict(version.payroll_properties)
# {'DISABLEDDEDUCTION': 2}
version._get_property_input_value('DISABLEDDEDUCTION')
# 2
payslip = env['hr.payslip'].create({
'employee_id': version.employee_id.id,
'date_from': date(2026, 9, 1),
'date_to': date(2026, 9, 30),
})
payslip.input_line_ids.mapped(lambda line: (line.code, line.amount))
# [('DISABLEDDEDUCTION', 2.0)]
version._set_property_input_value('NET', 3)
# ValidationError: Cannot set value for 'Net Salary': no version property definition
# found for this salary rule.
Work hours¶
The worked days of the payslips are computed from the work entries generated for the versions
(see Work entries).
get_work_hours() aggregates generated work entry values by work entry
type: it returns {(work entry type, category options): hours}. The keys are tuples of
records, not ids; the category options are an hr.salary.rule.category recordset, empty
in most cases. _preprocess_work_hours_data() is a hook to adapt these
hours in place.
Example
from datetime import date
work_entries = version.generate_work_entries(date(2026, 9, 1), date(2026, 9, 30))
dict(version.get_work_hours(date(2026, 9, 1), date(2026, 9, 30), work_entries))
# {(hr.work.entry.type(177,), hr.salary.rule.category()): 176.0}
_get_occupation_dates() groups consecutive versions of an employee
with the same employee type and work time rate (gaps shorter than 4 days are ignored) into
occupations: [(versions, date_from, date_to or False)]. Localizations use it to compute
seniority or the periods to declare.
Yearly cost¶
final_yearly_costs (Yearly Cost) is the yearly employer cost of the version. It is computed
from the wage by _get_yearly_cost_from_wage():
benefits costs + pay periods per year * wage / sacrifice ratio + fixed sacrifice
and it is editable: its inverse sets the wage giving that cost
(_get_wage_from_yearly_costs()). The terms are hooks overridden by
the localizations and the salary configurator (hr_contract_salary):
_get_benefits_costs(): the yearly cost of the salary benefits of the structure type (hr.contract.salary.benefitwith a cost field);_get_yearly_cost_sacrifice_ratio()(1 - holidays / 231by default) and_get_yearly_cost_sacrifice_fixed()(0), for the salary sacrifices (_get_sacrifice_fields(),holidaysby default);_get_benefit_fields(),_benefit_white_list()and_benefit_black_list(): the version fields usable as benefits, which are the dependencies offinal_yearly_costs.
Example
version.wage, version.final_yearly_costs
# (35000.0, 420000.0)
version._get_wage_from_yearly_costs(480000)
# 40000.0
Impact on the payslips¶
Writing on a version a field returned by
_get_fields_that_recompute_payslip() (wage, hourly_wage,
payroll_properties) refreshes the draft regular payslips of the current company overlapping
the version (_recompute_payslips()). Add your fields to this hook
when your salary rules read them.
Example
payslip.compute_sheet()
payslip.line_ids.filtered(lambda line: line.code == 'BASIC').total
# 35000.0
employee.wage = 40000 # writes the current version
payslip.line_ids.filtered(lambda line: line.code == 'BASIC').total
# 40000.0
A version with validated payslips cannot be deleted or archived.
Other payroll fields of the version: is_non_resident, disabled, holidays (extra paid time
off days per year), payroll_config_id (the payroll configuration valid at the version date),
issues (the payroll warnings concerning the version, see
Pay runs and warnings).
Employee¶
Besides the related version fields, hr_payroll adds to hr.employee:
slip_ids(payslips) andsalary_attachment_ids(payslip adjustments, see Payslip adjustments); archiving an employee closes its running adjustments;registration_number, unique per company;review_stateanddate_to_review: when a user who is not payroll officer changes a tracked field of the employee, the employee is flagged To Review for the payroll officers;tracking messages about payroll fields are posted with the
hr_payroll.mt_hr_payroll_sensitivesubtype, hidden to the users without payroll rights.