Get started¶
Installation¶
Requires Python 3.12+. Every pull request and every release run the API and knowledge tests on Python 3.12, 3.13 and 3.14, and smoke-test the built wheel on each.
Find a CCNL and its level¶
Employment takes the slug of a bundled CCNL and one of its level codes.
Search the catalog by name or slug, pick the contract by slug or CNEL code
and list its levels:
"""Find a CCNL and its level before the first run.
A caller does not need to know a file name: search the bundled contracts by
name or slug, pick one by slug or CNEL code, list its levels, then pass the
slug and a level code to :class:`Employment`.
"""
from ccnl_engine import Employment, PayrollEngine
from ccnl_engine.catalog import get_ccnl, search_ccnls
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
# Every bundled contract, or a search on name and slug.
print(f"Bundled CCNLs: {len(engine.list_contracts())}")
for candidate in search_ccnls("commercio"):
print(candidate.ccnl_id, candidate.cnel_code, candidate.name)
# By slug or by CNEL code. A CNEL code can cover more than one bundled
# contract (H052: Turismo Confcommercio, Federalberghi and Agenzie di viaggio
# Fiavet); a shared code raises UnknownCcnlError with the slugs to choose from.
contract = get_ccnl("H011")
print(f"\n{contract.name}: {contract.readiness}")
for level in engine.list_levels(contract.ccnl_id):
print(level.code, level.description)
employment = Employment(
ccnl_slug=contract.ccnl_id,
level_code="4",
contract_type=Permanent(),
)
print(f"\nEmployment on {employment.ccnl_slug}, level {employment.level_code}")
PayrollEngine.list_contracts() returns every contract; search_ccnls()
matches name and slug; get_ccnl() and engine.list_levels() take a slug or a
CNEL code. A CNEL code shared by several bundled contracts raises
UnknownCcnlError with their slugs as suggestions.
Quickstart¶
The minimum working example: load a CCNL, describe the employee, compute the payroll.
"""Quickstart: compute one payroll period with PayrollEngine."""
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodInput,
)
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
result = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=1),
payment_date=date(2026, 1, 28),
employment=Employment(
ccnl_slug="commercio-confcommercio.json",
level_code="4",
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(50)),
)
)
print(f"Gross: {result.period_gross} EUR")
print(f"Net: {result.period_net} EUR")
print(f"Cost: {result.period_employer_cost} EUR")
print(f"Payable: {result.is_payable} ({len(result.blockers)} blockers)")
# Readiness of the CCNL ruleset: the same before a run and in the result.
ruleset = engine.inspect_ruleset("commercio-confcommercio")
print(f"CCNL: {ruleset.id} is {ruleset.readiness}")
Every result says whether its amounts can be paid as computed
(result.is_payable, result.blockers) and which rulesets it read, with
their readiness (result.rulesets). engine.list_contracts() and
engine.inspect_ruleset(ccnl_id) report readiness before any run.
PayrollEngine.bundled() simulates; PayrollEngine.bundled(mode="operational")
computes the same amounts but blocks payment unless the CCNL ruleset is
production, which only Metalmeccanico Federmeccanica is. See
Readiness.
A full year¶
A full year derives its calendar from the CCNL: Commercio grants tredicesima
and quattordicesima, so the year has 14 runs. A different calendar needs a
CalendarOverride with a reason, and an override that drops a CCNL extra
month raises InvalidInputError. Employment.employment_period selects the
runs: a worker employed from July to September gets three runs, and the
September run also pays the 3/12 of tredicesima and quattordicesima accrued
until the termination. A month employed only in part pays the CCNL daily
quotas of its employed days (one twenty-sixth per Monday to Saturday for
most CCNLs); a CCNL whose data define no daily quota leaves that month
undetermined and not payable, never paid in full.
from ccnl_engine import CompetenceYearPlan, PeriodFacts
year = engine.calculate_competence_year(
CompetenceYearPlan(
year=2026,
employment=employment,
employer=employer,
default_facts=PeriodFacts(regione="IT-45"),
)
)
print(len(year.period_results)) # → 14
next_year = year.next_opening_state # close_tax_year of the closing state
The state keeps competence and cash apart: closing_state.accrual lists the
runs closed over the employment and the INPS base of each competence year,
closing_state.cash the payments and year-to-date totals of the tax year.
A run is paid on payment_day of its month unless payment_dates names its
date, or the CCNL fixes the day of an extra month (Commercio: the
tredicesima on Christmas Eve, the quattordicesima on 1 July).
A December paid after 12 January is a payment of the next tax year
(TUIR art. 51 c. 1): calculate_competence_year settles the conguaglio of
the year on its last payment actually made in it and opens the next tax
year with the late December, and calculate_tax_year(TaxYearPlan(...))
computes every payment cashed in one tax year, late payments of an earlier
competence year included. The bundle ships the tax tables of 2026 and
provisional tables of 2027 (ccnl_engine.catalog.supported_tax_years()).
The 2027 tables are provisional: the 2027 sources are not published
yet, so they carry the 2026 values over (RulesetIdentity.provisional).
Every run that reads them, a December 2026 paid after 12 January
included, is computed and not payable, with the open limitation
provisional_ruleset (tax) or provisional_inps_ruleset (INPS). A payment of any other tax year raises
UnsupportedTaxYearError. A run opens with the history of the employment:
PeriodState.zero() is the fact only for the first run of an employment
whose start is stated, and a run without its history is not payable.
Totals of another provider enter through
engine.import_opening_balances(OpeningBalances(...)), and the INPS base of
the worker's other employments of the year through
CurrentYearTaxFacts.other_employment_inps_base or the imported
inps_bases; see
Opening state and imported balances.
CompetenceYearPlan.periods maps a month (1-12) or a run id such as
"2026-12-thirteenth" to the PeriodFacts of that run; runs without an entry
take default_facts.
Supported CCNLs¶
Every bundled CCNL is listed in the contracts index with its CNEL code, sector, estimated workforce, readiness and coverage; the list and its counts are generated from the data.
Full list with sources and per-contract examples: Contracts →
To understand the terminology used in each contract page, read Domain: What is a CCNL first.
Disclaimer¶
Not legal or tax advice. Always verify results against official sources or a qualified payroll professional.