Skip to content

ccnl-engine

Python engine for auditable Italian payroll simulations: CCNL-aware gross-to-net and employer cost, versioned rules, provenance, and an explicit status on every result. Built for technical teams in HR, payroll, and compensation.

Demo · GitHub · CCNL coverage


Four pillars

Pillar What it covers
Domain How Italian labor law and CCNLs are represented as typed models
Rules The versioned knowledge base: CCNL tables, INPS rates, IRPEF brackets
Engine How rules are applied to produce a reproducible payroll figure
Trust Provenance, calculation status, versioning, and reference cases that make results verifiable

Documentation

Section Description
Get started Install, quickstart, and first payroll in 10 lines
Domain What CCNLs are, Italian labor law, employment types
Rules The versioned knowledge base: CCNL JSON schema, INPS, IRPEF, surtax
Engine How to use PayrollEngine — pay components, fiscal, domestic work
Trust Provenance, calculation status, capability report, versioning, quality gates
Data operations Update policy, release notes, error reporting, version compatibility
Correctness layers Software, source, and case correctness — what each layer means and how to read them
Contracts All 132 supported contracts — salary tables, sources, coverage
API reference Full reference for every public type and function

Why trust a number from this engine?

Every result carries three verifiability layers:

  1. Provenance: each rule links to its primary source document (CCNL article, INPS circular, tax schedule) with a URL, section, and verification status.
  2. Versioning: result.bundle_version records the knowledge-base version, so any figure can be reproduced after a CCNL renewal by pinning the package.
  3. Assurance and decisions: result.is_payable says whether the amounts can be paid as computed and result.blockers why not (an issue, a missing fact, a capability not computed, a rule without a located source, a caller-supplied rule, and in operational mode a CCNL ruleset that is not production); result.assurance holds the calculation, coverage and evidence axes, result.rulesets the rulesets read with their readiness, and result.decisions what each capability decided and from which inputs. An unknown normative fact never yields a payable result.
if not result.is_payable:
    for blocker in result.blockers:
        print(blocker.code.value, blocker.feature, blocker.detail)
for decision in result.decisions:
    print(decision.capability, decision.reason_code, decision.amount)

See the explanation example for a full walkthrough.


Scope

Gross and net (L1, L2):

  • IRPEF gross and net (Art. 11-13 TUIR), work income deductions, trattamento integrativo (Art. 1 D.L. 3/2020)
  • Regional and municipal income tax surcharges (addizionale regionale e comunale IRPEF)
  • INPS contributions (employee and employer), resolved by headcount tier
  • TFR accrual
  • Contractual employer funds
  • Part-time scaling, seniority increments, fixed allowances
  • Apprenticeship contracts (under-classification and percentage tracks)
  • Fixed-term contracts (NASpI addizionale)
  • Domestic work (flat per-hour contributions, non-withholding employer)

Second-level (territorial and company) allowances are not an engine input.

What each contract computes, and its limits, is in the capability matrix.

Work rules (L3):

Work events are part of the run: pass them in PeriodFacts.events and they change gross, net and employer cost according to their treatment. See Work rules.

  • Overtime pay (lavoro straordinario) with the CCNL band or a caller multiplier: OvertimeEvent
  • Night, holiday and shift supplements, with the 2026 substitute tax: NightShiftEvent, HolidayWorkEvent, ShiftWorkEvent
  • Unpaid absences and sickness: AbsenceEvent, SicknessEpisode, SickLeaveEvent (override)
  • Bonuses (ordinary, premio di risultato with its flat tax, renewal increments): BonusEvent
  • Welfare and fringe benefits: WelfareEvent, FringeEvent

Further facts:

  • Family-dependent deductions (Art. 12 TUIR): PeriodFacts.family_composition with dated dependency intervals (omitted, the deductions are undecided and the result not payable; FamilyComposition() states no dependant), and PeriodInput.current_year (CurrentYearTaxFacts) for the income beyond this employment
  • Regional and municipal surtax: PeriodFacts.regione and comune_belfiore (omitted, the surtax is undecided and the result not payable; a code without a table makes the result incomplete)
  • Prior-year income and written waivers for the substitute-tax regimes: PriorYearTaxFacts

Why this project

I never really understood employment contracts or pay slips. The whole system strikes me as needlessly complicated. On top of that, finding reliable CCNL information online feels like an uphill battle: conflicting figures are everywhere, and identifying authoritative sources is harder than it should be. This project grew out of a desire to understand a little more. I make no claim to becoming an expert, but I hope to make this information more accessible and comprehensible for everyone.


Disclaimer

Not legal or tax advice. Always verify results against official sources or a qualified payroll professional.