Pay components¶
This page covers the employment facts that adjust gross pay beyond the CCNL table minimum: part-time scaling, seniority and worker category. It also covers bilateral fund contributions, which change net pay and employer cost.
See Domain: Components for the legal background.
Part-time¶
Pass the contracted weekly_hours together with the CCNL
full_time_weekly_hours on Employment, as WeeklyHours. The engine derives the
part-time fraction from the two and scales the contractual pay by it.
weekly_hours must not exceed full_time_weekly_hours. The contracted hours
alone never count as full time: without full_time_weekly_hours the run
computes the full-time pay and carries a missing_fact blocker for it, on
a domestic CCNL too, where weekly_hours also selects the INPS bracket.
The bracket of up to 24 weekly hours is chosen on the paga oraria effettiva
(INPS circ. 9/2026): the monthly pay with board and lodging, spread over
weekly_hours x 52 / 12 hours and raised by the tredicesima. The
tredicesima run charges no INPS hours of its own.
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodInput,
)
from ccnl_engine.inputs import Permanent, WeeklyHours
engine = PayrollEngine.bundled()
def gross(weekly_hours: WeeklyHours | None = None) -> str:
employment = Employment(
ccnl_slug="metalmeccanico-federmeccanica.json",
level_code="C3",
contract_type=Permanent(),
weekly_hours=weekly_hours,
full_time_weekly_hours=WeeklyHours(40),
)
result = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=3),
payment_date=date(2026, 3, 27),
employment=employment,
employer=EmployerProfile(headcount=Headcount(50)),
)
)
return str(result.period_gross)
print("Full time:", gross())
print("Half time:", gross(WeeklyHours(20)))
Role allowances¶
Some allowances of a level are paid only to a worker holding a role, e.g.
IND_FUNZIONE_QUADRO of Alimentari 1S for the role quadro. State the roles
on Employment.roles, a frozenset of role codes; frozenset() states that
the worker holds none. roles=None, the default, means not known: on a level
with an allowance restricted to a role in force, the run leaves the
allowance out and has a missing_fact blocker for roles. A worker
category, QUADRO included, does not unlock a role allowance.
Seniority increments (scatti di anzianità)¶
Pass the recognised seniority as Employment.seniority, a SeniorityFact:
the completed months of service on a date (as_of) and where they were read
from (SenioritySource). SeniorityFact.since(date, source) states the date
the recognised service starts instead. The engine ages the fact to each run:
a run counts the months completed by the first day of its competence month,
so an increment matured during a month is paid from the next one; service
that starts within the month (a hire on the 15th recognised from that day)
counts zero months, and a run of a month before the recognised service
raises InvalidInputError. It derives
the number of matured increments from the CCNL cadence and adds the amount
the level earns.
Every run records a seniority decision with one of four reasons:
| Reason | When | Amount |
|---|---|---|
not_applicable_by_contract |
The level pays no increment to the worker: no amount for the level, a zero maximum or an excluded category (e.g. operai edili, paid through the Cassa Edile) | 0 |
zero_confirmed |
The seniority is known and no increment has matured | 0 |
increments_applied |
The seniority is known and increments are paid | the increments |
required_fact_missing |
seniority=None on a level that pays increments, or an allowance gated by months of service |
None |
The fact is required only when the level pays increments to the worker's
category (an unknown category counts when any category is paid) or holds an
allowance gated by months of service for the worker's roles. Without it the
decision is provisional, the seniority_unknown issue names the fact, the
result has a missing_fact blocker for seniority and is not payable. The
amounts of such a run leave the increments and the gated allowances out:
they are what a worker without seniority would earn, not the answer.
"""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}")
Worker category¶
Some CCNLs price seniority by legal category (art. 2095 c.c.): in Servizi
Postali in Appalto FISE an operaio and an impiegato on the same level earn
different increments. Pass the category as Employment.category, a
WorkerCategory (operaio, impiegato, quadro, dirigente). The same
value selects category-specific INPS employer rates (e.g. impiegati in
artigianato).
- A level reserved to one category (e.g. a
quadrolevel) supplies it when you omit it; declaring a different category raisesInvalidInputError. - When increments for the level exist only per category and you pass
senioritywithout a category, the calculation raisesInvalidInputErrorinstead of silently dropping the increment.
TFR¶
Each run accrues the TFR quota of art. 2120 c.c. (the TFR base over 13.5),
less the 0.50% additional IVS of L. 297/1982 art. 3 c. 16 where the sector
deducts it (an apprentice owes none: INPS circ. 70/2007, note 5). Two facts
of Employment decide where it goes and how the fund grows.
Fondo Tesoreria¶
tfr_treasury_fund says whether the TFR not paid to a pension fund goes to
the Fondo Tesoreria INPS (L. 296/2006 art. 1 cc. 755-756). The obligation
does not follow the headcount of the run: it is the yearly average of 2006,
or of the year the activity started (DM 30 gennaio 2007 art. 1 c. 6), and
from 2026 also the average of the year before for an employer that grows
past 50, with at least 60 employees in 2026 and 2027 (c. 756 as in force
from 12 August 2026). Some workers are excluded whatever the size (DM art. 1
c. 8: fixed-term contracts under three months, home workers, agricultural
white collars insured with ENPAIA). State the outcome:
tfr_treasury_fund |
Account of the TFR | Blocker |
|---|---|---|
True |
tfr_treasury_fund |
none |
False |
tfr_accrual |
none |
None |
tfr_accrual |
missing_fact tfr_treasury_fund on a run with a non-zero TFR outside a pension fund |
The amount is the same on both accounts. Domestic employers and public
administrations are outside the Fondo: None and False keep the TFR in
the company, True raises InvalidInputError. The TFR a worker pays to a
pension fund goes to pension_fund_tfr whatever the fact says.
Compensations of the TFR conferred¶
When the TFR of a run goes to a pension fund or to the Fondo Tesoreria, the employer contributions of the run fall by two exemptions on the INPS base, in the same percentage as the TFR conferred (the engine confers the TFR of a run whole, so 100%):
| Component | Rate | Source |
|---|---|---|
tfr_guarantee_fund_exemption |
0.20%, 0.40% for a dirigente of industria or edilizia | D.Lgs. 252/2005 art. 10 c. 2; INPS circ. 70/2007 par. 6 |
tfr_relief_exemption |
0.28 points | D.L. 203/2005 art. 8 and Tabella A; INPS circ. 4/2008 |
Both are negative employer components of the contribution breakdown and
never take the employer contributions below zero. They apply in industria,
terziario, artigianato, credito and edilizia; domestic work and public
administrations are outside the Fondo di garanzia, and agricoltura, whose
rates by contract are not modelled, takes none. While the Fondo Tesoreria
destination is unknown (None) no exemption applies. An apprentice takes
the 0.28 points only: whether the apprentice rate holds a Fondo di garanzia
share to exempt is not sourced, and the run records the open limitation
tfr_compensation_apprentice_guarantee_fund.
Revaluation at 31 December¶
tfr_fund is a TfrFundBalance(year, amount): the TFR the worker has with
the employer at 31 December of year, after that day's revaluation and
substitute tax and less the advances paid, including the part at the Fondo
Tesoreria and excluding the part at a pension fund. The December regular
run revalues it (art. 2120 c. 4 c.c.): the quota accrued in the year is
excluded, the rate is 1.5% plus 75% of the increase of the ISTAT FOI index
without tobacco from December to December (L. 81/1992 art. 4 c. 1; across a
change of index base the ratio is multiplied by ISTAT's link coefficient,
1.214 from base 2015 to base 2025). The revaluation bears a 17% substitute
tax charged to the fund (D.Lgs. 47/2000 art. 11 cc. 3-4).
The run records a tfr_revaluation decision:
| Reason | When | Amount |
|---|---|---|
revalued |
Fund of the year before stated, December index bundled | Revaluation; substitute_tax and net_revaluation in the inputs |
no_opening_fund |
A zero fund, or tfr_fund=None on an employment that starts in the year |
0 |
required_fact_missing |
tfr_fund=None on an employment that started before the year or is not tracked |
None, missing_fact tfr_fund |
fund_of_another_year |
tfr_fund.year is not the year before the run |
None, missing_fact tfr_fund |
price_index_not_published |
The bundle has no December index of the year | None |
termination_not_computed |
The run ends the employment before 31 December (art. 2120 c. 5: a fraction of the year) | None |
negative_rate |
The index fell enough to make the rate negative: no source says how it applies | None |
Every reason but the first two leaves the decision incomplete and the run
not payable. ISTAT publishes the December 2026 index in mid-January 2027:
until the bundle carries it, every December 2026 run with a fund to revalue
is blocked. The decision is not posted to the ledger: the revaluation
changes the fund, not the pay, the net or the employer cost of the run.
An employment that starts in the year is taken to have no fund. When it
carries a TFR over (a transfer of undertaking under art. 2112 c.c., or a
rehire whose TFR moved with the worker), state tfr_fund anyway.
The engine never outputs the fund. Carry it to the next year yourself:
tfr_fund of the next year = tfr_fund + net_revaluation + the
tfr_accrual and tfr_treasury_fund postings of the year - the advances
paid.
Not computed: the revaluation for a fraction of the year at termination,
the acconto of the substitute tax (90% by 16 December) and its saldo (16
February), the share of the revaluation the Fondo Tesoreria bears and the
employer recovers, and the TFR of public employees, which INPS manages:
tfr_fund is still required on their December runs.
from datetime import date
from decimal import Decimal
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodInput,
)
from ccnl_engine.inputs import EmploymentPeriod, Permanent, TfrFundBalance
engine = PayrollEngine.bundled()
result = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=12),
payment_date=date(2026, 12, 23),
employment=Employment(
ccnl_slug="metalmeccanico-federmeccanica.json",
level_code="C3",
contract_type=Permanent(),
employment_period=EmploymentPeriod(date(2020, 1, 1)),
tfr_fund=TfrFundBalance(2025, Decimal("15000.00")),
tfr_treasury_fund=True,
),
employer=EmployerProfile(headcount=Headcount(60)),
)
)
(revaluation,) = (d for d in result.decisions if d.capability == "tfr_revaluation")
print(revaluation.reason_code) # price_index_not_published until January 2027
Bilateral funds (fondi bilaterali)¶
Many CCNLs require contributions to sector bilateral bodies (health funds,
training funds). The engine does not derive them from the CCNL: pass the
amounts due in the period as a BilateralFundEvent. The employee portion
reduces net pay; the employer portion increases employer cost. For the
pension fund of the CCNL use the enrolment described in
Pension funds instead. A CCNL that
fixes its contribution per paid hour in the bundle (the Cas.Sa.Colf of the
CCNL lavoro domestico, see Domestic work) is charged by the
engine itself, on the same accounts, under the assistance_contribution
capability.
from datetime import date
from decimal import Decimal
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodFacts,
PeriodInput,
)
from ccnl_engine.events import BilateralFundEvent
from ccnl_engine.inputs import Permanent
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="metalmeccanico-federmeccanica.json",
level_code="C3",
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(50)),
facts=PeriodFacts(
events=(
BilateralFundEvent(
event_date=date(2026, 3, 1),
employee_amount=Decimal("2.00"),
employer_amount=Decimal("13.00"),
),
),
),
)
)
print(result.period_net, result.period_employer_cost)
Pension funds (previdenza complementare)¶
Enrolment in a complementary pension fund is voluntary (D.Lgs. 252/2005
art. 1 c. 2), so the engine never assumes it. Declare it on the employment
as Employment.pension_fund, a PensionFundEnrolment:
fund_code: a fund of the CCNL, e.g."ALIFOND"for Tabacco,"FONCHIM"for Vetro meccanizzato or"FONTE"for Commercio. A code the CCNL does not declare raisesInvalidInputError.employee_rate: the contribution the worker chose. It cannot be below the CCNL minimum when the bundle records one (ALIFOND: 1%, Fon.Te.: 0.55%).tfr_to_fund: whether the TFR accrued is paid to the fund. Required, with no default.
pension_fund=NoPensionFund() states that the worker is not enrolled: no
fund line is posted. On a CCNL that has a fund, the
pension_fund_contribution decision records the reason not_enrolled and
the capability is not applicable. pension_fund=None, the default, means
not known: on every CCNL but domestic work, whether or not the bundle holds
its negotiated fund, the decision is incomplete with the reason
required_fact_missing, no fund line is posted and the run has a
missing_fact blocker for pension_fund. The bundle holds the fund data of
thirteen CCNLs (Cometa for metalmeccanico Federmeccanica, Alifond for
tabacco and alimentari Federalimentare, Fondapi
for alimentari PMI, Fonchim for chimica farmaceutica and vetro
meccanizzato, Prevedi for edilizia industria and artigianato, and Fon.Te. for
commercio, turismo Confcommercio and Federalberghi, pubblici esercizi FIPE
and agenzie di viaggio FIAVET): an enrolment in the
fund of another CCNL raises InvalidInputError, since its rates are not in
the bundle. Whether the TFR of a worker who
expressed no choice goes to the fund (silent consent, D.Lgs. 252/2005 art.
8 c. 7) is for the caller to establish: the engine does not infer it.
When enrolled, each run posts:
| Line | Account | Amount | Effect |
|---|---|---|---|
| Employer contribution | pension_fund_employer |
CCNL rate x fund base of the run | employer cost |
| Solidarity contribution | employer_contributions |
10% of the employer contribution | employer cost |
| Employee contribution | pension_fund_employee |
chosen rate x fund base | withheld from net |
| TFR to the fund | pension_fund_tfr instead of tfr_accrual or tfr_treasury_fund |
TFR of the run, net of the 0.50% additional IVS (L. 297/1982 art. 3 c. 16) | none: the cost does not change |
The rules behind it:
- Base. Each fund of
CCNL.parameters.employer_fundsnames the base its rates apply to incontribution_base:inps_base, the default, is the INPS contribution base of the run, events included;tfr_baseis the pay that enters the TFR of the run (the monthly pay, the benefits in kind and the events the TFR includes), the base of every fund in the bundle (Fon.Te., Alifond, Fondapi on the food PMI, Fonchim). Fonchim's employer rate includes the 0.25% the employer pays to the fund for the insurance of premorienza and invalidity. Overtime and bonuses enter the INPS base and not the TFR base. - Rates by contract. A fund can charge apprentices another employer
rate (
apprentice_rate, used whencontract_typeis anApprentice). Fon.Te. takes the rates of each CCNL from Allegato 1 of its nota informativa: 1.55% employer on Commercio (1.05% for apprentices), 0.55% on Turismo, Pubblici esercizi and Agenzie di viaggio; the worker pays at least 0.55% everywhere. - Deduction. Employee and employer contributions are deductible from
the taxable income up to 5 300.00 EUR a year from tax year 2026
(D.Lgs. 252/2005 art. 8 c. 4 as amended by L. 199/2025; TUIR art. 10
c. 1 lett. e-bis and art. 51 c. 2 lett. h). Within the cap the taxable
falls by the employee part; beyond it the employer part is taxable
income. The TFR paid to the fund does not count. The amount already
deducted is tracked in
closing_state.cash.earnings.pension_deducted(OpeningBalances.pension_deductedwhen importing a year in progress). - Solidarity. The employer contributions, the TFR excluded, stay
outside the INPS base and bear the 10% solidarity contribution to INPS
(D.Lgs. 252/2005 art. 16 c. 1; art. 9-bis D.L. 103/1991, conv. L.
166/1991). It is posted to
employer_contributionsin the ledger but is not a component ofcontribution_breakdown, which holds the INPS contributions on the pay only.
A CCNL can also owe its fund a fixed contribution a month for every
worker, enrolled or not (CCNL.parameters.contractual_fund_contribution,
an amount per level). The Materiali da costruzione CCNL (lapidei) owes
Fondapi 5 EUR "riparametrati su base 100": 6.80 EUR for level 5. It is an
employer contribution to the fund: posted to pension_fund_employer with
its 10% solidarity, within the deduction cap, on every run that pays the
month and on no extra-month run. A worker not enrolled has the decision
reason contractual_only; an enrolled one has it in the contractual
input, on top of the ordinary rates. The sources give no rule for a partial
month or part time: the run pays the full amount and has the open
limitation contractual_fund_partial.
The clause can carry the rules of its fund. Prevedi, on Edilizia industria
and artigianato, pays the impiegati and quadri 10 to 20.50 EUR a month by
level (apprentices 10), only for a month of at least 15 calendar days worked
(sickness and days without pay left out), in proportion to part time, on
the tredicesima and quattordicesima in proportion to their ratei, and not
for a fixed term of three months or less unless the worker pays voluntary
contributions (CNCE vademecum; accordo of 4 July 2025). The operai pay the
hourly amount of their level (0.0685 to 0.1027 EUR, apprentices 0.0700)
times PeriodFacts.ordinary_hours_worked, the ordinary hours actually
worked in the month, rounded to the euro, and nothing on an extra month:
an operaio qualificato of the industria with 160 hours owes 0.0801 x 160 =
12.82, 13 EUR. Without the hours the run has the issue
contractual_fund_hours_unknown; a level the table has no row for,
contractual_fund_not_computed; a level that leaves the category open,
contractual_fund_category_unknown.
Not modelled: the compensatory measures for employers whose TFR goes to a
fund (D.Lgs. 252/2005 art. 10), the extra deduction of workers first
employed from 2007 (art. 8 c. 6), a partial TFR conferment, and the eligibility
conditions some CCNLs set (e.g. ALIFOND excludes fixed-term contracts up
to six months). Fondapi on tessile PMI and metalmeccanico PMI computes on the contractual
minimum too; its base also counts the EDR (and, for the metalmeccanico, the
indennita di funzione of the quadri and the elemento of the 8th and 9th
categories), which the bundle pay lacks: an enrolled run has the open
limitation fondapi_base_elements and is not payable.
A fund can raise the employer rate with the rate the worker chooses
(EmployerFund.employer_rate_tiers): Fondapi on the chemical PMI pays
1.66% on the TFR base, 2.00% once the worker contributes at least 1.60%
(minimum 1.06%).
A fund due on the twelve monthly payments alone has
EmployerFund.extra_months false: an extra-month run of an enrolled worker
has a zero base. Byblos on the CCNL Esercizi cinematografici (art. 43: 1%
employer and 1% employee "per 12 mensilità annue") is one.
Byblos on the CCNL grafici editoriali pays the employer 1.9% of the TFR base
(the retribuzione contrattuale annua), 1.4% for a holder of the Elemento di
Raccordo Contrattuale (EmployerFund.erc_holder_rate), whose employer rate
the renewal of 19 January 2021 did not raise. State the annual ERC in
Employment.erc_amount, zero when the worker has none; an employment
started after December 2020 has none. Left None otherwise, an enrolled run
uses 1.9% and has the issue pension_fund_erc_unknown. The
same amount is paid with the tredicesima (see the extra months in the
engine overview).
Previambiente, on the CCNL Servizi Ambientali (art. 65 lett. A), computes
2.033% employer and at least 1.30% employee on a conventional base
(contribution_base conventional): the base pay of the level at 1
January 1997, its contingenza and one scatto. The CCNL tables give it for
the levels before the classification of 1 February 2026 only, so the
enrolment states it in PensionFundEnrolment.conventional_base; left
None, the rates are left out with the issue
pension_fund_conventional_base_unknown. A higher employee rate goes on the
TFR base. An enrolled worker adds 22 EUR a month (30.50 EUR from January
2027, EmployerFund.enrolled_monthly); every member owes the 5 EUR
insurance, and a worker not enrolled on a permanent contract or an
apprenticeship 10 EUR more (ContractualFundContribution.not_enrolled_monthly,
permanent_only), on 12 monthly payments. A month without pay owes nothing
of c. 5 (paid_month_only); a month paid in part has the open limitation
fund_paid_month. A new hire may convert the seniority increments into
contributions (lett. A) bis, PensionFundEnrolment.seniority_to_fund): the
pay holds no increment, and the employer pays the fund the amount of each
increment matured, at most 10 (EmployerFund.seniority_conversion: the
increment x 1.10 x 14 / 12, 50.27 EUR for a quadro), on 12 monthly
payments. A worker already in service states the date of the request in
PensionFundEnrolment.seniority_converted_on: the increments matured by
then stay in the pay, frozen, and only the later ones are converted (c. 6).
A worker who confers the TFR alone states employee_rate zero (with
tfr_to_fund): neither side contributes, the TFR goes to the fund, and a
contractual contribution owed to a worker not enrolled voluntarily is owed
(Previambiente art. 65 c. 12: 15 EUR). A public employee cannot: Perseo
Sirio and Espero allow it to the private employees alone.
Perseo Sirio, on the CCNLs of the public administrations, pays 1% employer
and at least 1% employee of the TFR base. The TFR a public employee confers
is not paid to the fund: INPS Gestione Dipendenti Pubblici accrues it
notionally and pays it at the termination: with
Employment.public_end_of_service tfr_inps the run posts no TFR, and the
pension decision records tfr_to_fund as notional. The 1.5%
of an optant is notional too and not computed. Fondo Scuola Espero, on the
school CCNL and for the dirigenti scolastici, works the same way (1% + 1%,
the 1.2% of an optant notional); the dirigenza Istruzione e Ricerca holds
both funds and the enrolment names one.
Cometa computes on the contractual minimum of the level
(contribution_base contractual_minimum, the base salary of the pay
chain): employer 2%, employee at least 1.2%. A higher employee rate goes on
the TFR base (employee_base_above_minimum). A member enrolled after 5
February 2021 before turning 35 has an employer rate of 2.2%: state it in
PensionFundEnrolment.young_member. Left None, the run uses 2% and has
the issue pension_fund_young_member_unknown. The 40% TFR quota of a worker
employed before 28 April 1993 is not modelled.
"""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))
API reference: Employment,
WorkerCategory