Examples¶
Runnable scripts covering every major feature of the engine.
Each file in docs/examples/ is executed in CI via tests/integration/ccnl_engine/api/test_docs_examples.py:
if an API change breaks an example, the build fails.
Trust — Why this number?¶
The most important question a payroll engine must answer is not "what is the net salary?" but "why is it that number?" This example walks through all three verifiability layers: provenance, versioning, and the capability report.
"""Explain a result: pay items, contributions, F24 remittance, capability report."""
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodFacts,
PeriodInput,
)
from ccnl_engine.inputs import SeniorityFact, SenioritySource
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="metalmeccanico-federmeccanica.json",
level_code="C3",
seniority=SeniorityFact(36, date(2026, 1, 1), SenioritySource.PAYSLIP),
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(100)),
facts=PeriodFacts(regione="IT-45", comune_belfiore="F257"),
)
)
print("=== Pay items ===")
for item in result.pay_items:
print(f" {item.kind:40s} {item.amount}")
print("\n=== Contribution components ===")
for c in result.contribution_breakdown.components:
print(f" {c.name:30s} base={c.base} rate={c.rate} amount={c.amount}")
print("\n=== Tax computation ===")
tc = result.tax_computation
print(f" Ordinary IRPEF: {tc.ordinary_tax}")
print(f" Trattamento integrativo: {tc.trattamento_integrativo}")
print(f" Withholding due: {tc.withholding_due}")
print("\n=== F24 remittance by codice tributo ===")
for line in result.remittance_summary():
code = line.remittance_code or "-"
column = line.column or "-"
print(f" {line.account:26s} {code:5s} {column:7s} {line.amount}")
print("\n=== Assurance ===")
assurance = result.assurance
print(f" Payable: {result.is_payable}")
print(f" Calculation: {assurance.calculation} coverage: {assurance.coverage}")
print(f" Evidence: {assurance.evidence}")
for ruleset in result.rulesets:
readiness = ruleset.readiness or "not tracked"
print(f" ruleset: {ruleset} {ruleset.kind} readiness: {readiness}")
for blocker in result.blockers:
print(f" blocker: {blocker.code.value:24s} {blocker.feature} {blocker.detail}")
Basics¶
Quickstart¶
Minimal call: build a PeriodInput and pass it to PayrollEngine.calculate_period().
"""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}")
Reading the result¶
PayrollEngine.calculate_period() returns a PeriodResult for one run: its status, period
gross, net and employer cost, the INPS contribution breakdown, the IRPEF
computation and the pay items.
"""Payroll result fields: gross, net, contributions, pay items."""
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=6),
payment_date=date(2026, 6, 27),
employment=Employment(
ccnl_slug="metalmeccanico-federmeccanica.json",
level_code="C3",
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(100)),
)
)
print(f"Period gross: {result.period_gross}")
print(f"Period net: {result.period_net}")
print(f"Employer cost: {result.period_employer_cost}")
cb = result.contribution_breakdown
print(f"Employee INPS: {cb.employee}")
print(f"Employer INPS: {cb.employer}")
tc = result.tax_computation
print(f"IRPEF withheld: {tc.ordinary_tax}")
print(f"Trattamento integr.: {tc.trattamento_integrativo}")
for comp in tc.components:
print(f" {comp.name}: {comp.amount} ({comp.fonte})")
for item in result.pay_items:
print(f" {item.kind}: {item.amount}")
Contract types¶
Fixed-term (tempo determinato)¶
FixedTerm(renewals=..., naspi_exclusion=...) adds the NASpI surcharge to the employer's INPS contribution: 1.4%, plus 0.5% per renewal, unless an exclusion of L. 92/2012 art. 2 c. 29 applies; gross and net are unchanged. Left unknown, either fact blocks the run.
"""Fixed-term employment renewed once: the NASpI surcharge is 1.4% + 0.5%."""
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodInput,
)
from ccnl_engine.inputs import FixedTerm, NaspiExclusion
engine = PayrollEngine.bundled()
result = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=3),
payment_date=date(2026, 3, 27),
employment=Employment(
ccnl_slug="commercio-confcommercio.json",
level_code="4",
# Second fixed-term contract with the employer, no exclusion of
# L. 92/2012 art. 2 c. 29: surcharge 1.4% + 0.5% (c. 28).
contract_type=FixedTerm(renewals=1, naspi_exclusion=NaspiExclusion.NONE),
),
employer=EmployerProfile(headcount=Headcount(50)),
)
)
print(f"Period gross: {result.period_gross}")
print(f"Period net: {result.period_net}")
print(f"Employer cost (includes the NASpI surcharge): {result.period_employer_cost}")
(employer,) = (d for d in result.decisions if d.capability == "inps_employer")
print(f"NASpI surcharge rate: {employer.inputs['naspi_surcharge_rate']}")
Apprenticeship (apprendistato)¶
Percentage track: the apprentice's pay is a % of the destination level, increasing with months_elapsed.
"""Apprenticeship: reduced salary percentage from CCNL percentage track."""
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodInput,
)
from ccnl_engine.inputs import Apprentice
engine = PayrollEngine.bundled()
def apprentice_period(months_elapsed: int) -> PeriodInput:
"""Build a Commercio level 4 apprentice run for January 2026.
Returns:
The period input at ``months_elapsed`` months of apprenticeship.
"""
return 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=Apprentice(months_elapsed=months_elapsed),
),
employer=EmployerProfile(headcount=Headcount(50)),
)
# Month 0: start of apprenticeship (lowest percentage)
result_start = engine.calculate_period(apprentice_period(0))
# Month 24: further into the track (higher percentage)
result_24 = engine.calculate_period(apprentice_period(24))
print(f"Gross at month 0: {result_start.period_gross}")
print(f"Gross at month 24: {result_24.period_gross}")
Pay components¶
Family deductions¶
A FamilyComposition with dependants raises the Art. 12 TUIR deductions and the net pay. The deductions read the reddito complessivo of the year: state the income beyond this employment with CurrentYearTaxFacts (zero included), or the result is not payable. See Family deductions.
"""Family deductions (Art. 12 TUIR): dependent spouse and children reduce net IRPEF.
The deductions depend on the reddito complessivo of the year, so the run
needs the income beyond this employment: ``CurrentYearTaxFacts``, zero
included. Without it the deductions are a simulation and the result is not
payable. Children under 21 are covered by the assegno unico and give no
Art. 12 deduction; the one born in 2004 turned 21 in 2025, so it gives the
deduction for the whole of 2026.
Every condition of a dependant is stated: own income (art. 12 c. 2),
residency (c. 2-bis), the share of a child (lett. c: the whole deduction,
because the spouse is a dependant of the worker) and the dependency interval
(``None`` for open ends). A condition left ``None`` is unknown: the
dependant takes no deduction and the run has a ``missing_fact`` blocker.
"""
from datetime import date
from decimal import Decimal
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodFacts,
PeriodInput,
)
from ccnl_engine.inputs import (
CurrentYearTaxFacts,
Dependent,
DependentRelationship,
FamilyComposition,
)
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
employment = Employment(
ccnl_slug="commercio-confcommercio.json", level_code="4", contract_type=Permanent()
)
employer = EmployerProfile(headcount=Headcount(50))
run = PayrollRun.regular(year=2026, month=1)
payment = date(2026, 1, 28)
# No dependents
result_single = engine.calculate_period(
PeriodInput(
run=run,
payment_date=payment,
employment=employment,
employer=employer,
facts=PeriodFacts(regione="IT-45", family_composition=FamilyComposition()),
)
)
# Dependent spouse, a child of 22 and a minor child; no other income in 2026
result_family = engine.calculate_period(
PeriodInput(
run=run,
payment_date=payment,
employment=employment,
employer=employer,
facts=PeriodFacts(
regione="IT-45",
family_composition=FamilyComposition(
dependents=(
Dependent(
relationship=DependentRelationship.SPOUSE,
own_income=Decimal(0),
residency_eligibility=True,
dependent_from=None,
dependent_until=None,
),
*(
Dependent(
relationship=DependentRelationship.CHILD,
birth_date=born,
own_income=Decimal(0),
allocation_pct=Decimal(100),
residency_eligibility=True,
dependent_from=None,
dependent_until=None,
)
for born in (date(2004, 5, 10), date(2018, 8, 20))
),
)
),
),
current_year=CurrentYearTaxFacts.employment_only(2026, date(2026, 1, 2)),
)
)
(family,) = [d for d in result_family.decisions if d.capability == "family_deductions"]
print(f"Net (no dependents): {result_single.period_net}")
print(f"Net (family): {result_family.period_net}")
print(f"Art. 12 uplift: {result_family.period_net - result_single.period_net}")
print(f"Annual deductions: {family.amount} ({family.inputs['months']})")
Seniority increments (scatti di anzianità)¶
Pass the recognised seniority as Employment.seniority, a SeniorityFact dated and sourced; unknown seniority is a missing fact, not zero. The category selects category-specific increments.
"""Seniority increments: state the recognised seniority as a dated fact."""
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodInput,
)
from ccnl_engine.inputs import SeniorityFact, SenioritySource, WorkerCategory
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
run = PayrollRun.regular(year=2026, month=1)
payment = date(2026, 1, 28)
employer = EmployerProfile(headcount=Headcount(100))
def seniority_reason(seniority: SeniorityFact | None) -> None:
"""Print the gross, the seniority decision and the payability of a run."""
result = engine.calculate_period(
PeriodInput(
run=run,
payment_date=payment,
employment=Employment(
ccnl_slug="metalmeccanico-federmeccanica.json",
level_code="C3",
seniority=seniority,
contract_type=Permanent(),
),
employer=employer,
)
)
(decision,) = (d for d in result.decisions if d.capability == "seniority")
print(
f"{result.period_gross} {decision.reason_code:22s} "
f"amount={decision.amount} payable={result.is_payable}"
)
# Unknown: the increments are undetermined, a missing_fact blocker names it.
seniority_reason(None)
# A new hire: zero increments, confirmed.
seniority_reason(SeniorityFact.since(date(2026, 1, 1), SenioritySource.PAYSLIP))
# 60 months on 1 January 2026: the engine ages the fact to each run.
seniority_reason(SeniorityFact(60, date(2026, 1, 1), SenioritySource.PAYSLIP))
# Category-specific increments: FISE level 2 at 60 months of service pays
# 56.66 to an operaio and 62.62 to an impiegato.
for category in (WorkerCategory.OPERAIO, WorkerCategory.IMPIEGATO):
result = engine.calculate_period(
PeriodInput(
run=run,
payment_date=payment,
employment=Employment(
ccnl_slug="servizi-postali-appalto-fise.json",
level_code="2",
seniority=SeniorityFact(
60, date(2026, 1, 1), SenioritySource.EMPLOYER_RECORDS
),
category=category,
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(50)),
)
)
print(f"FISE L2 {category}: {result.period_gross}")
Pension fund (previdenza complementare)¶
Declare the enrolment in the CCNL fund as Employment.pension_fund; without it no fund contribution is posted. See Pension funds.
"""Pension fund: enrol the worker in the CCNL fund to post its contributions."""
from datetime import date
from decimal import Decimal
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodInput,
)
from ccnl_engine.inputs import NoPensionFund, PensionFundEnrolment, Permanent
engine = PayrollEngine.bundled()
def run(pension_fund: PensionFundEnrolment | NoPensionFund) -> None:
"""Print the fund lines, net and employer cost of January 2026."""
result = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=1),
payment_date=date(2026, 1, 27),
employment=Employment(
ccnl_slug="tabacco-apti.json",
level_code="4A",
pension_fund=pension_fund,
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(50)),
)
)
for entry in result.ledger_entries:
if entry.account.startswith("pension_fund"):
print(f" {entry.account:<22} {entry.amount:>8}")
print(f" net {result.period_net} employer cost {result.period_employer_cost}")
print("Not enrolled:")
run(NoPensionFund())
print("Enrolled in ALIFOND, 1% employee, TFR to the fund:")
run(PensionFundEnrolment("ALIFOND", Decimal("0.01"), tfr_to_fund=True))
Year-to-date chaining¶
Pass the closing_state of one run as the opening_state of the next, so progressive IRPEF and the year-to-date totals carry forward.
"""YTD state chaining: pass closing_state from one run as opening_state for the next."""
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodFacts,
PeriodInput,
)
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
employment = Employment(
ccnl_slug="metalmeccanico-federmeccanica.json",
level_code="C3",
contract_type=Permanent(),
)
employer = EmployerProfile(headcount=Headcount(100))
facts = PeriodFacts(regione="IT-45")
# January: opening_state defaults to zero
jan = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=1),
payment_date=date(2026, 1, 28),
employment=employment,
employer=employer,
facts=facts,
)
)
# February: carry forward YTD state from January
feb = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=2),
payment_date=date(2026, 2, 27),
employment=employment,
employer=employer,
facts=facts,
opening_state=jan.closing_state,
)
)
print(f"Jan gross: {jan.period_gross} net: {jan.period_net}")
print(f"Feb gross: {feb.period_gross} net: {feb.period_net}")
# Inspect YTD accumulators after February
state = feb.closing_state
print(f"YTD taxable income: {state.cash.earnings.taxable}")
print(f"YTD IRPEF withheld: {state.cash.tax.irpef}")
Second-level bargaining (contrattazione di secondo livello)¶
Territorial or company allowances on top of the CCNL minimums, with per-item contribution/TFR/apprenticeship control.
"""Full-year run: standard calendar derived from the CCNL, then an override.
Without ``calendar_override`` the engine runs the calendar the CCNL grants. A custom
calendar is accepted only as a ``CalendarOverride`` with a domain reason; an
override that drops or lowers a CCNL extra month raises ``InvalidInputError``.
"""
from ccnl_engine import (
CompetenceYearPlan,
EmployerProfile,
Employment,
Headcount,
InvalidInputError,
PayrollEngine,
)
from ccnl_engine.inputs import CalendarOverride, CalendarOverrideReason, WorkCalendar
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
def year_request(calendar: CalendarOverride | None = None) -> CompetenceYearPlan:
"""Build a Commercio level 4 input for 2026.
Returns:
The year input, with ``calendar`` as the optional override.
"""
return CompetenceYearPlan(
year=2026,
employment=Employment(
ccnl_slug="commercio-confcommercio.json",
level_code="4",
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(50)),
calendar_override=calendar,
)
# Standard calendar: tredicesima in December, quattordicesima in June.
result = engine.calculate_competence_year(year_request())
print(f"Annual gross: {result.annual_gross}")
print(f"Annual net: {result.annual_net}")
print(f"Annual employer cost:{result.annual_employer_cost}")
print(f"Runs computed: {len(result.period_results)}")
for r in result.period_results:
run_label = r.run.run_id if r.run else str(r.period_id)
print(f" {run_label:30s} gross={r.period_gross}")
# Same entitlement, quattordicesima paid with the July salary.
july = CalendarOverride(
calendar=WorkCalendar.from_additional_months(2026, 14, fourteenth_payment_month=7),
reason=CalendarOverrideReason.PAYMENT_MONTH,
note="quattordicesima paid with the July salary",
)
moved = engine.calculate_competence_year(year_request(july))
print(f"Runs with July quattordicesima: {len(moved.period_results)}")
# Dropping the quattordicesima is rejected whatever the reason.
only_thirteenth = CalendarOverride(
calendar=WorkCalendar.from_additional_months(2026, 13),
reason=CalendarOverrideReason.PAYMENT_MONTH,
note="attempt to skip the quattordicesima",
)
try:
engine.calculate_competence_year(year_request(only_thirteenth))
except InvalidInputError as exc:
print(f"Rejected: {exc}")
Fiscal¶
Regional and municipal surcharges (addizionali)¶
Pass regione (ISO 3166-2:IT region code, e.g. IT-45) and comune_belfiore (Belfiore code, e.g. F257) to include addizionale regionale and comunale. The surtax of a year is determined by its conguaglio and withheld the next year by installments, so a run withholds the surtax an earlier conguaglio determined: import it with OpeningBalances.surtax_obligations. Check result.assurance.calculation and result.decisions: an unknown table, or a regione or comune_belfiore left None (decision residence_unknown), makes the calculation incomplete and the result not payable.
"""Regional and municipal surtax: withheld the year after the conguaglio."""
from dataclasses import replace
from datetime import date
from decimal import Decimal
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodFacts,
PeriodInput,
)
from ccnl_engine.inputs import (
FamilyComposition,
InpsBaseYtd,
OpeningBalances,
SeniorityFact,
SenioritySource,
SurtaxComponent,
SurtaxObligation,
)
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
# The 2025 conguaglio, run by the previous provider, determined the 2025
# regional surtax and municipal saldo and the 2026 municipal acconto.
# import_opening_balances is the one entry point for totals the engine did
# not compute.
opening = engine.import_opening_balances(
OpeningBalances(
tax_year=2026,
surtax_obligations=(
SurtaxObligation.open(
SurtaxComponent.REGIONAL_BALANCE, 2025, "IT-45", Decimal("330.00")
),
SurtaxObligation.open(
SurtaxComponent.MUNICIPAL_BALANCE, 2025, "F257", Decimal("110.00")
),
SurtaxObligation.open(
SurtaxComponent.MUNICIPAL_ADVANCE, 2025, "F257", Decimal("45.00")
),
),
# No other employment of the worker in 2026, no recovery running.
inps_bases=(InpsBaseYtd(2026, Decimal(0), Decimal(0)),),
recoveries=(),
)
)
request = PeriodInput(
run=PayrollRun.regular(year=2026, month=1),
payment_date=date(2026, 1, 27),
employment=Employment(
ccnl_slug="metalmeccanico-federmeccanica.json",
level_code="C3",
seniority=SeniorityFact(36, date(2026, 1, 1), SenioritySource.PAYSLIP),
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(100)),
facts=PeriodFacts(
regione="IT-45", # Emilia-Romagna
comune_belfiore="F257", # Modena
family_composition=FamilyComposition(), # no dependant
),
opening_state=opening,
)
result = engine.calculate_period(request)
# January, the first run of 2026 after the import, withholds one installment
# of each 2025 saldo, at most 11 from January (D.Lgs. 446/1997 art. 50 c. 4,
# D.Lgs. 360/1998 art. 1 c. 5): 330.00 / 11 = 30.00 (3802) and
# 110.00 / 11 = 10.00 (3848). The 2026 acconto starts in March (art. 1 c. 5).
for line in result.remittance_summary():
if line.account == "surtax":
print(f" {line.remittance_code}: {line.amount}")
# The 2026 surtax is determined only by the conguaglio of 2026 and withheld
# in 2027: before it, each annual decision is determined_at_conguaglio.
for decision in result.decisions:
annual = "component" not in decision.inputs
if decision.capability.startswith("addizionale_") and annual:
print(f" {decision.capability}: {decision.reason_code}")
# What is left of the imported debts after January.
for obligation in result.closing_state.cash.obligations.surtax:
print(f" {obligation.component}: residual {obligation.plan.residual}")
# Whether the run can be paid is the assurance, not the surtax lines: list
# what blocks it, if anything.
print(f"Calculation: {result.assurance.calculation}, payable: {result.is_payable}")
for blocker in result.blockers:
print(f" {blocker.code}: {blocker.feature} {blocker.detail}")
# Without the residence the surtax of 2026 is undetermined, not zero: each
# missing code records residence_unknown and names the fact to supply.
unknown = engine.calculate_period(
replace(request, facts=PeriodFacts(family_composition=FamilyComposition()))
)
for decision in unknown.decisions:
if decision.reason_code == "residence_unknown":
print(f" {decision.capability}: supply {decision.inputs['fact']}")
print(f"Calculation: {unknown.assurance.calculation}") # incomplete
December paid in January (tax year and conguaglio)¶
A tax year holds the payments made in it (TUIR art. 51 c. 1): December paid
by 12 January is still income of its year, paid later it opens the next one.
calculate_tax_year computes the payments of one tax year and settles the
conguaglio on the last; calculate_competence_year computes the runs of one
competence year across the tax years that pay them.
"""December paid in January: competence years, tax years and the conguaglio.
The December salary of 2026 paid on 13 January 2027 is income of tax year
2027 (TUIR art. 51 c. 1): tax year 2026 holds the other thirteen payments
and settles its conguaglio on the tredicesima; 2027 opens with the late
December. Paid on 12 January it would still be 2026 income, and the last
payment, hence the conguaglio, of 2026.
"""
from datetime import date
from ccnl_engine import (
CompetenceYearPlan,
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
TaxYearPlan,
)
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
employment = Employment(
ccnl_slug="commercio-confcommercio.json", level_code="4", contract_type=Permanent()
)
employer = EmployerProfile(headcount=Headcount(50))
def plan(december: date) -> CompetenceYearPlan:
"""Return the 2026 plan with the December salary paid on ``december``.
Returns:
The competence-year plan.
"""
return CompetenceYearPlan(
year=2026,
employment=employment,
employer=employer,
payment_dates={12: december},
)
# The tax year 2026: the payments actually made in it. December paid on 13
# January 2027 belongs to 2027, so the tredicesima settles the conguaglio.
late = engine.calculate_tax_year(
TaxYearPlan(tax_year=2026, competence_years=(plan(date(2027, 1, 13)),))
)
print("13 January:", len(late.payments), "payments, conguaglio", late.conguaglio)
print("2027 opens with tax year", late.next_opening_state.tax_year)
# Paid on 12 January it is 2026 income: the competence year is also the
# tax year, and the December salary settles the conguaglio.
on_time = engine.calculate_competence_year(plan(date(2027, 1, 12)))
print(
"12 January:",
len(on_time.period_results),
"payments, conguaglio",
on_time.conguagli[0],
)
print("difference in 2026 gross:", on_time.annual_gross - late.annual_gross)
# With the 2027 tables, TaxYearPlan(tax_year=2027, competence_years=(the 2026
# plan, the 2027 plan), opening_state=late.next_opening_state) computes the
# late December first, then the 2027 runs; the last one settles 2027.
Domestic work (lavoro domestico)¶
Flat per-hour INPS contributions; the employer does not withhold IRPEF. weekly_hours is required.
"""Domestic work (CCNL colf): flat per-hour INPS, Cas.Sa.Colf, no withholding."""
from datetime import date
from decimal import Decimal
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodFacts,
PeriodInput,
)
from ccnl_engine.inputs import ContributableHours, WeeklyHours
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
# Non-convivente domestic worker, level B, 25 h of a 40-hour week (the
# week of the non-convivente minimum), 108 hours in January
result = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=1),
payment_date=date(2026, 1, 28),
employment=Employment(
ccnl_slug="lavoro-domestico-non-convivente.json",
level_code="B",
weekly_hours=WeeklyHours(25),
full_time_weekly_hours=WeeklyHours(40),
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(1)),
facts=PeriodFacts(contributable_hours=ContributableHours(Decimal(108))),
)
)
print(f"Period gross: {result.period_gross}")
print(f"Period net: {result.period_net}")
print(f"Period employer cost: {result.period_employer_cost}")
cb = result.contribution_breakdown
print(f"Employee INPS: {cb.employee}")
print(f"Employer INPS: {cb.employer}")
# Cas.Sa.Colf (CCNL art. 54 c. 2): 0.02 per hour withheld, 0.04 paid by the employer
assistance = next(
d for d in result.decisions if d.capability == "assistance_contribution"
)
print(f"Cas.Sa.Colf worker: {assistance.inputs['employee']}")
print(f"Cas.Sa.Colf employer: {assistance.inputs['employer']}")
# A household employer is not a withholding agent: net = gross - employee shares
skipped = [
d.capability for d in result.decisions if d.reason_code == "not_withholding_agent"
]
print(f"Not withheld: {', '.join(skipped)}")