ugaroll
A standalone Uganda payroll engine for Python. Handles PAYE, NSSF, Local Service Tax (LST), payslips, and paysheets — all with zero dependencies.
Features
basic,gross, ornetdriven payroll requests- Built-in preset categories: overtime, bonus, transport, SACCO, housing benefit, gratuity, and more
- Simplified
from_hrm()factory for quick category creation with justtaxable,nssfable, andnature - Percentage-based line items (% of basic or % of gross)
- Batch payroll processing with aggregate totals
- Resident, non-resident, secondary-employment, and secondary-above-10M tax modes
- Configurable PAYE, NSSF, and LST policies
- HTML payslip and paysheet rendering out of the box
Installation
pip install ugaroll
Quick Example — Single Payslip
from uganda_payroll import (
EmployerProfile,
EmployeeProfile,
EmploymentTaxMode,
InputMode,
PayrollCalculator,
PayrollLineItem,
PayrollPeriod,
PayrollRequest,
PaymentCategory,
render_payslip_html,
)
calculator = PayrollCalculator()
# Use preset categories — no need to configure effects manually
overtime = PaymentCategory.overtime()
sacco = PaymentCategory.sacco_deduction()
request = PayrollRequest(
employer=EmployerProfile(name="Nkozi Farms Ltd", tin="1005827461"),
employee=EmployeeProfile(
full_name="Grace Atuhaire",
staff_number="7023",
tin="1028463915",
nssf_number="8842100557031",
department="Logistics",
position="Fleet Coordinator",
employment_tax_mode=EmploymentTaxMode.PRIMARY_RESIDENT,
),
period=PayrollPeriod.from_month(year=2026, month=3),
input_mode=InputMode.BASIC_PAY,
target_amount=2_450_000,
items=[
PayrollLineItem(category=overtime, amount=185_000),
PayrollLineItem(category=sacco, amount=50_000),
],
)
result = calculator.calculate(request)
payslip_html = render_payslip_html(result)
print(f"Gross: {result.gross_earnings:,}")
print(f"PAYE: {result.total_paye:,}")
print(f"NSSF: {result.nssf_employee:,}")
print(f"Net: {result.net_pay:,}")
# Save payslip to file
with open("payslip_grace.html", "w") as f:
f.write(payslip_html)
Paysheet — Multiple Employees
from uganda_payroll import (
AmountMode,
EmployerProfile,
EmployeeProfile,
InputMode,
PaymentCategory,
PayrollBatchRequest,
PayrollCalculator,
PayrollLineItem,
PayrollPeriod,
PayrollRequest,
render_paysheet_html,
)
calculator = PayrollCalculator()
employer = EmployerProfile(name="Nkozi Farms Ltd", tin="1005827461")
period = PayrollPeriod.from_month(2026, 3)
bonus = PaymentCategory.bonus()
transport = PaymentCategory.transport()
overtime = PaymentCategory.overtime()
requests = [
PayrollRequest(
employer=employer,
employee=EmployeeProfile(
full_name="Grace Atuhaire", staff_number="7023",
tin="1028463915", department="Logistics",
),
period=period,
input_mode=InputMode.BASIC_PAY,
target_amount=2_450_000,
items=[PayrollLineItem(category=overtime, amount=185_000)],
),
PayrollRequest(
employer=employer,
employee=EmployeeProfile(
full_name="Kenneth Obol", staff_number="7041",
tin="1033918204", department="Engineering",
),
period=period,
input_mode=InputMode.BASIC_PAY,
target_amount=4_100_000,
items=[
PayrollLineItem(category=bonus, amount=300_000),
PayrollLineItem(category=transport, amount=120_000),
],
),
PayrollRequest(
employer=employer,
employee=EmployeeProfile(
full_name="Esther Nambooze", staff_number="7055",
tin="1041672583", department="Finance",
),
period=period,
input_mode=InputMode.BASIC_PAY,
target_amount=1_800_000,
items=[
# 5% of basic as overtime
PayrollLineItem(
category=overtime, amount=5,
amount_mode=AmountMode.PERCENTAGE_OF_BASIC,
),
],
),
]
# Calculate individually and render a paysheet
results = [calculator.calculate(r) for r in requests]
paysheet_html = render_paysheet_html(results, title="March 2026 Paysheet — Nkozi Farms Ltd")
with open("paysheet_march.html", "w") as f:
f.write(paysheet_html)
# Or use batch processing for aggregate totals
batch = PayrollBatchRequest(
employer=employer,
period=period,
employee_requests=tuple(requests),
)
batch_result = calculator.calculate_batch(batch)
print(f"Total Net Pay: {batch_result.total_net_pay:,}")
print(f"Total PAYE: {batch_result.total_paye:,}")
print(f"Total NSSF (EE): {batch_result.total_nssf_employee:,}")
print(f"Total NSSF (ER): {batch_result.total_nssf_employer:,}")
print(f"Total Employer Cost: {batch_result.total_employer_cost:,}")
Preset Categories
Create common Ugandan payroll categories in one line:
from uganda_payroll import PaymentCategory
PaymentCategory.overtime() # Taxable + NSSF
PaymentCategory.bonus() # Taxable + NSSF
PaymentCategory.transport() # Non-taxable, no NSSF
PaymentCategory.housing_benefit() # Taxable memo (non-cash benefit)
PaymentCategory.sacco_deduction() # Post-tax deduction
PaymentCategory.salary_advance() # Post-tax deduction
PaymentCategory.payroll_correction() # Reduces PAYE base, not NSSF
PaymentCategory.medical_insurance() # Employer contribution
PaymentCategory.gratuity() # Post-tax addition
PaymentCategory.pension_deduction() # Pre-tax, reduces PAYE base
PaymentCategory.notice_pay() # Taxable + NSSF
PaymentCategory.leave_compensation() # Taxable + NSSF
All presets accept optional code and name overrides:
PaymentCategory.overtime(code="ot_weekend", name="Weekend Overtime")
Simplified Category Creation
If you don't need full control over every effect flag, use from_hrm() to create categories with just three fields:
from uganda_payroll import PaymentCategory
# A non-taxable meal allowance
meal = PaymentCategory.from_hrm(
code="meal",
name="Meal Allowance",
nature="Increment",
taxable=False,
nssfable=False,
)
# A taxable, NSSF-attracting commission
commission = PaymentCategory.from_hrm(
code="comm",
name="Sales Commission",
nature="Increment",
taxable=True,
nssfable=True,
)
Supported nature values: "Increment", "Deduction", "Increment2", "Do nothing", "Bonus"
Notes on Defaults
- Resident monthly PAYE bands match current URA schedules
- Secondary employment: 30% flat, or 40% if all employments exceed UGX 10M
- Non-resident rates are configurable; the default uses 10% for the first bracket
- LST is configurable — the default mirrors common "Chop Once" / "Chop Four times" patterns
- NSSF: 5% employee, 10% employer on total employment income (basic + all qualifying allowances)
Metadata
Release files for ugaroll 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ugaroll-0.2.2.tar.gz | 17.3 kB | Without provenance | Details for ugaroll-0.2.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ugaroll-0.2.2-py3-none-any.whl | Python 3 | none | any | Details for ugaroll-0.2.2-py3-none-any.whl |
Total release size: 32.1 kB
Release files / ugaroll-0.2.2.tar.gz
| Download URL | ugaroll-0.2.2.tar.gz |
|---|---|
| Size | 17.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9047246968ba5ff5ddc8b41b862a1a24d711d68d350a3fe60fecdddaf4354732
|
|
BLAKE2b-256 checksum How to use checksums |
59214b3c9880d0bf8ed19a7149dd79fdb66ecaa6f2a2f230b6a38fc7e0f40aea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / ugaroll-0.2.2-py3-none-any.whl
| Download URL | ugaroll-0.2.2-py3-none-any.whl |
|---|---|
| Size | 14.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4944918cf3d1a6075fe59483fa05b8681fa06b4ca65223c768fb2fb5681006c9
|
|
BLAKE2b-256 checksum How to use checksums |
3b8991e8a843058c8c931864536ad70091b1852ba6877d0cfdf372ebc4871adc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Security for ugaroll 0.2.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.