All posts
Integrator13 min read

Odoo HR and payroll: employees, contracts, payslips, and leave management for implementors

Odoo HR covers the full employee lifecycle - from recruitment through payroll and departure. This guide explains the data model, contract and payslip mechanics, leave allocation logic, and the common configuration mistakes that cause silent payroll errors.

Odoo HR is one of the most interconnected modules in the suite. Payslip generation depends on contracts, which depend on salary structures, which depend on salary rules, which in turn may reference leave balances, timesheets, and analytic accounts. A misconfigured rule causes incorrect payslips, and incorrect payslips cause compliance failures. Understanding the full chain before touching configuration is the difference between a clean implementation and months of corrections.

This guide walks through the HR data model, payroll mechanics, leave management, and the configuration decisions that cause the most trouble.

The employee data model#

Every person in Odoo HR is an hr.employee record. The key fields for payroll:

FieldPurpose
company_idWhich company runs payroll for this employee
department_idDepartment for reporting and approval chains
job_idJob position (for headcount planning)
contract_idsOne2many of hr.contract records
address_home_idPrivate address (res.partner) - used for payslip PDF
km_home_workDistance home-to-work; referenced in some salary rules
resource_idLinks to resource.resource for work schedule and leaves

The resource_id relationship is subtle. Every employee has exactly one resource.resource, which in turn has a resource.calendar (work schedule). Leaves, timesheets, and attendance all interact through the resource layer. If you change an employee's work schedule, check whether their leave allocations still make sense for the new hours.

Contracts and salary structures#

Payslips are always generated from an active contract. There is no payslip without a contract in open state that covers the payslip period.

python
# hr.contract key fields
class HrContract(models.Model):
    _name = 'hr.contract'

    date_start = fields.Date()
    date_end = fields.Date()  # empty = indefinite
    state = fields.Selection([
        ('draft', 'New'),
        ('open', 'Running'),
        ('close', 'Expired'),
        ('cancel', 'Cancelled'),
    ])
    wage = fields.Monetary()  # gross monthly wage
    structure_type_id = fields.Many2one('hr.payroll.structure.type')
    struct_id = fields.Many2one('hr.payroll.structure')

The structure_type_id controls which salary structures are available for the contract. The struct_id picks one structure for the payslip. Most implementations use a single structure per contract type - for example, one structure for salaried employees, another for hourly workers.

Salary structures and rules#

A salary structure (hr.payroll.structure) contains an ordered list of salary rules (hr.salary.rule). When a payslip is computed, Odoo evaluates each rule in sequence and accumulates the result.

Each rule has:

  • Condition - Python expression that returns True/False. If False, the rule is skipped.
  • Amount type - Fixed, Percentage (of another rule), or Code (arbitrary Python).
  • Category - groups rules for the payslip PDF (BASIC, GROSS, NET, deductions, etc.).

The computation context for rule Python code includes:

python
# Available variables in rule code
employee   # hr.employee browse record
contract   # hr.contract browse record
payslip    # hr.payslip browse record
rules      # dict of {rule_code: computed_amount} for already-evaluated rules
categories # dict of {category_code: total} for categories already computed
worked_days # dict of {work_entry_type_code: hr.payslip.worked.days}
inputs     # dict of {input_code: hr.payslip.input}

This means a rule can reference any field on the contract (like contract.wage) or the result of any previously computed rule (like rules['BASIC'].amount). The order of rules in the structure matters - you cannot reference a rule that has not yet been computed.

Payslip generation#

Payslips (hr.payslip) are generated one of two ways: individually through HR → Payroll → Payslips, or in batch through HR → Payroll → Payslip Batches.

Batch generation is the normal production path. A batch is created for a specific date range and a list of employees or a department. Odoo creates one draft payslip per employee, then you validate the batch to compute all payslips at once.

Work entry types#

Odoo 14+ uses work entries (hr.work.entry) to represent all time: attendance, leaves, public holidays, overtime. Work entries are generated from the work schedule and leave records. When computing a payslip, Odoo reads the work entries in the payslip period to populate the worked_days lines.

A common source of payslip errors: leaves that are validated after the payslip is computed are not included. If you validate a leave on the same day you run payroll, check whether the work entries were regenerated before the payslip was computed.

To regenerate work entries manually: HR → Payroll → Work Entries, select the employee and date range, then click Regenerate.

Leave management#

Leaves are built on top of hr.leave (leave request) and hr.leave.allocation (entitlement). The distinction matters:

  • An allocation gives an employee N days of leave type X for period Y.
  • A leave request consumes days from an allocation.

Leave types#

Leave types (hr.leave.type) control:

SettingEffect
Allocation modeManual (HR creates allocations), Fixed by HR, or Accrual
Request typeDay (full days) or Hour (fractions)
ApprovalNo validation, Line manager, or HR officer
Carry-overWhether unused days roll into next year
Time off type for payrollWork entry type used when computing payslips

The Work entry type field on the leave type is the link between leave management and payroll. If you create a custom leave type but don't set this field, leaves of that type are ignored by payslip computation.

Accrual plans#

Accrual plans define how leave days accumulate over time. An accrual plan has one or more levels, each with:

  • Rate (e.g., 1.25 days per month)
  • Frequency (daily, weekly, monthly)
  • Cap (maximum days the accrual can reach)
  • Carryover limit

Accruals are computed by a nightly cron job. If you create an allocation with an accrual plan and the employee's balance is wrong, check whether the cron hr_payroll.ir_cron_accrual_plans ran since the allocation was created.

Common configuration mistakes#

Wrong work entry type on leave type. Leaves are recorded but payslips don't deduct them. Fix: set the correct hr.work.entry.type on the leave type. The work entry type must have is_leave = True to appear in the salary rule context.

Overlapping contracts. Odoo does not prevent two open contracts from overlapping in time. The payslip generator picks one contract, but the choice is non-deterministic if two are valid for the same period. Always archive the old contract before opening the new one.

Salary rules referencing undefined codes. If a rule references rules['BONUS'] but no rule with code BONUS exists in the structure, the rule raises a KeyError at compute time. The payslip fails silently - it stays in draft and shows no lines. Check the payslip chatter for a traceback.

Public holidays not configured for the right company. Public holidays in Odoo are global by default but can be scoped to resource calendars. If your company uses a country-specific calendar, verify that public holiday lines are attached to the correct calendar. Employees whose calendar doesn't include a public holiday will still be expected to work that day.

Leave requests approved but not work entries regenerated before payroll. The payslip sees the pre-leave work entries and computes full attendance. Force regeneration before each payroll run.

Payroll reporting#

Odoo includes several built-in payroll reports: the Payslip Analysis pivot (amounts per rule category per employee per period), and the Payroll Journal Entries report. For compliance reports (e.g., country-specific withholding declarations), you typically need a localization module (l10n_XX_hr_payroll). Install the localization before implementing payroll - retrofitting it later causes rule code conflicts.


For analytic account links between timesheets, projects, and payroll costs, see the project management and timesheets guide. For the subscription and recurring billing patterns that drive deferred compensation models, see the subscription billing guide.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free