Running statutory payroll across African countries on one ERPNext
Eleven countries, eleven currencies, eleven sets of tax bands that change every budget cycle. Hardcoding that into payroll is a maintenance trap. Here is the architecture that scales.
A group operating in Kenya, Uganda, and Tanzania does not have one payroll problem. It has three, and they share almost nothing. Kenya runs NSSF on tiered bands with caps, PAYE with personal relief applied as a tax credit, plus SHIF and the Housing Levy. Uganda has its own PAYE schedule, NSSF, and a Local Service Tax. Tanzania has yet another structure. Each country's rates change on its own budget cycle, and each statutory body wants its own report in its own format.
Hardcoding each country's logic into salary calculations, or standing up a separate payroll app per country, fails the first time a budget changes the bands. The model that scales treats country as a strategy you select at runtime, not a branch you copy and paste. Here is how we structure statutory payroll across eleven African countries on a single ERPNext install.
One hook, not a fork of HRMS
The first principle is to extend HRMS, never modify it. Frappe HRMS already owns the Salary Slip, its components, and the payroll run. Statutory deductions hook into that lifecycle through a single doc_event on Salary Slip validation:
Everything statutory flows from that one entry point. Because it is a validate hook on the standard Salary Slip, you inherit the entire HRMS payroll engine, leave management, attendance, salary structures, untouched and upgradeable. When HRMS releases a new version, you have nothing to merge. That is what keeps the build maintainable over years.
Country as a strategy, resolved at runtime
Inside that hook, the flow resolves which country's rules apply and delegates to a calculator built for exactly that country:
A registry maps each country to its calculator class and its settings DocType. Calculators are lazy-loaded and cached, so the Kenya calculator is only imported and instantiated when a Kenyan slip is processed. Adding a country is a registration, not a rewrite. You slot a new entry into the map and the engine picks it up.
Each calculator inherits from an abstract BaseCalculator and implements one method, compute, which returns a dictionary of components to amounts. Eleven concrete implementations live behind that interface, and the engine never needs to know which one it is talking to. The variation, per-country tax rules, sits behind a stable contract: compute a deduction set for this slip.
What the calculators actually encode
The country-specific logic is more subtle than a flat percentage, which is why it belongs in dedicated, testable classes rather than scattered through salary structure formulas:
- Progressive PAYE bands with cumulative rate application. Each slice of income is taxed at its own rate, not the whole amount at the top rate.
- Tier-based social security with caps. Kenya's NSSF Tier I and Tier II each have ceilings the calculation has to respect.
- Personal relief applied as a tax credit, not a deduction. It reduces tax owed after the bands are applied, which changes the arithmetic.
- Employee versus employer contribution splits. An is_employer_only flag marks contributions that are a company cost rather than an employee deduction, so they never reduce net pay but still show on statutory reports.
The employer-only distinction is the one that quietly produces wrong numbers when teams roll their own payroll. If an employer NSSF contribution is treated as an employee deduction, net pay is understated and the employee is effectively charged for the company's obligation. Model the split explicitly from the start.
Settings and components, kept country-aware
Rates live in data, not code. Each country gets a single settings DocType, Kenya Payroll Settings, Uganda Payroll Settings, and so on, holding its current rates, plus child tables for the PAYE bands. When a budget changes a band, you edit a record; you do not deploy.
Salary components are namespaced by country so the same chart can carry all of them without collision. The convention is simple: Kenya components carry no suffix (PAYE, NSSF Employee), Uganda components get a UG suffix (PAYE UG, NSSF UG), Tanzania gets TZ, and so on. That small discipline lets a single company run mixed-country payroll while keeping each country's components cleanly filterable.
Rate changes mid-cycle
Statutory rates do not change on your release schedule. A finance act can shift PAYE bands or a levy rate with little notice, sometimes backdated. If you have already generated draft salary slips for the period, they now hold stale numbers.
This is why a bulk recalculation path matters. A well-built payroll layer exposes an API to recompute draft slips for a country and date range after a rate change, rather than forcing payroll staff to delete and regenerate by hand:
The simulation API is worth calling out too. Being able to compute net pay for a hypothetical salary in a given country, without creating any document, turns offer-letter modelling and what-if analysis into a single call instead of a throwaway payroll run.
Reporting is half the value
Computing the right deduction is necessary but not sufficient. Every statutory body wants its filing in its own shape: Kenya's P9A and P10, NSSF, SHIF, and Housing Levy returns; Uganda's URA PAYE, NSSF, and LST; Tanzania's TRA and NSSF schedules; and so on across each country you operate in. A mature setup ships dozens of these country-specific reports plus cross-country rollups, statutory summaries, employer-contribution totals, true cost-to-company, and a rate-change audit trail.
Those reports are where payroll staff spend their time, so they are where the compliance value actually lands. Computing PAYE correctly but leaving someone to reformat it into a P10 by hand every month has only solved half the problem.
The payoff
Structured this way, onboarding a new country is a contained, repeatable job: write a calculator, add its settings DocType and band tables, register it, enable it, and add its reports. The core engine does not change, HRMS does not change, and the eleven countries already live keep running exactly as before. A security and guarding group we work with runs guard-force payroll across multiple countries on this single backbone, with each statutory regime handled by its own calculator behind one shared Salary Slip hook.
Multi-country payroll is one of the hardest things to get right on an ERP when it is done carelessly. The way to keep it sane is to stop country differences from leaking into the core. Keep the variation behind a clean interface, keep the rates in data, and the eleventh country costs about the same to add as the second.
Operating across more than one African market and wrestling with statutory payroll? We run this across eleven countries today. Book a discovery call and we will map your countries, your components, and your statutory reports to a single ERPNext payroll.
Working on something like this?
We ship ERPNext and custom Frappe apps across Kenya, Uganda, Tanzania, Rwanda, Ethiopia, and Somalia. Let's talk through your build.