Payroll¶
Backend domain: app/graphql/payroll/
Migration: add_payroll
RBAC Path: PAYROLL
Overview¶
Five tables:
Employee— auto-numberedEMP-00001, base salary, pay frequency (WEEKLY | BIWEEKLY | MONTHLY), national ID, position, department, optional bank account.PayrollPeriod— start/end + pay date. Statuses:DRAFT | CALCULATED | APPROVED | PAID | POSTED.PayrollRun— one per period; aggregate totals (gross, net, employer cost), status, optionalledgerEntryIdonce posted.PayrollItem— one per employee per run; stores gross, net, deductions JSONB, employer_contributions JSONB, optional hours/bonuses.PayrollConcept— a code (e.g.wage_expense,css_employee,isr_payable, …) → GL account mapping used at posting time.
Panama-specific calculations live in app/graphql/payroll/strategies/panama_payroll.py: CSS 9.75% employee / 12.25% employer, Seguro Educativo 1.25% / 1.5%, Riesgo Profesional 0.98% (employer), and a 3-bracket annual ISR table (0% / 15% / 25%).
Setup prerequisites¶
Critical: before postPayrollRun works, the tenant must have PayrollConcept rows seeded for at least these codes (each mapping to an existing GL account):
| Code | Kind | Typical account |
|---|---|---|
wage_expense |
EARNING | Wages & salaries expense |
css_employee |
DEDUCTION | (offsetting reference; not used for posting if you collapse to payable) |
css_employee_payable |
DEDUCTION | CSS payable |
css_employer |
EMPLOYER_CONTRIBUTION | CSS employer expense |
css_employer_payable |
EMPLOYER_CONTRIBUTION | CSS payable |
seguro_educativo_employee_payable |
DEDUCTION | Seguro Educativo payable |
seguro_educativo_employer |
EMPLOYER_CONTRIBUTION | SE employer expense |
seguro_educativo_employer_payable |
EMPLOYER_CONTRIBUTION | SE payable |
isr_payable |
DEDUCTION | ISR retention payable |
riesgo_profesional_employer |
EMPLOYER_CONTRIBUTION | Riesgo Profesional expense |
riesgo_profesional_employer_payable |
EMPLOYER_CONTRIBUTION | RP payable |
The posting code raises a ValueError with the missing code when something is unconfigured — surface as an actionable error and link to the setup screen.
There is no createPayrollConcept mutation yet — seed via DB or add the mutation as a follow-up.
GraphQL surface¶
Queries¶
employees: [Employee!]!
employee(employeeId: UUID!): Employee!
payrollPeriods: [PayrollPeriod!]!
payrollRun(runId: UUID!): PayrollRun!
payrollRunsForPeriod(periodId: UUID!): [PayrollRun!]!
Employee: id, employeeNumber, firstName, lastName, nationalId, hireDate, terminationDate, position, department, baseSalary, payFrequency, bankAccountId, status, isActive.
PayrollPeriod: id, startDate, endDate, payDate, status.
PayrollRun: id, payrollPeriodId, totalGross, totalNet, totalEmployerCost, ledgerEntryId, status, plus nested items: [PayrollItem!]!.
PayrollItem: id, runId, employeeId, gross, net, workedHours, overtimeHours, bonuses, deductions (JSON), employerContributions (JSON).
Mutations¶
createEmployee(employee: EmployeeInput!): Employee!
createPayrollPeriod(payrollPeriod: PayrollPeriodInput!): PayrollPeriod!
calculatePayroll(periodId: UUID!): PayrollRun!
approvePayrollRun(runId: UUID!): PayrollRun!
postPayrollRun(runId: UUID!, cashAccountId: UUID!): PayrollRun!
EmployeeInput: firstName, lastName, hireDate, baseSalary, plus optional payFrequency (default BIWEEKLY), nationalId, position, department, bankAccountId.
PayrollPeriodInput: startDate, endDate, payDate.
Behaviour to handle in the UI¶
- Employee directory — list, add, edit (no update mutation yet — see follow-up), terminate. Show
employeeNumberprominently. - Payroll period workspace — one screen per period with three tabs: Setup, Calculate, Post.
- Setup: period dates, pay date, list of active employees that will be included.
- Calculate: runs
calculatePayroll(periodId), shows the resultingPayrollRunwith one row per employee, expandable to show the JSON deductions/contributions per item. - Post:
approvePayrollRun→postPayrollRun(cashAccountId). Confirm the cash account. - Deductions/contributions JSON rendering: parse the JSON and display as a sub-table with code, label (from
PayrollConcept.name), amount. - No payslip PDF yet — see follow-up. For now offer "Print" using the calculated table.
Error states¶
| Behaviour | When | Frontend message |
|---|---|---|
ValueError ("PayrollConcept '' is not configured") |
postPayrollRun |
"Configure '' in Payroll Settings before posting." Link to the setup page. |
ValueError ("must be APPROVED") |
postPayrollRun on a non-approved run |
"Approve the run first." |
PeriodClosedError (from underlying ledger service) |
postPayrollRun against a closed accounting period |
"The accounting period covering MM/YYYY is closed." |
Suggested UX flow¶
- Employees page — directory + add/edit/terminate.
- Payroll Settings —
PayrollConceptmapping table (code → account). Block payroll posting until all required codes exist. - Payroll cycle — period list → workspace with the three tabs.
- History — list of all runs across periods with totals and posted JE links.
Payslip PDF + SIPE export¶
payslipPdfUrl returns a presigned URL to a per-employee PDF showing the period dates, gross/net, hours, bonuses, and an itemized breakdown of deductions and employer contributions parsed from the PayrollItem.deductions / employer_contributions JSONB blobs.
sipeExportUrl returns a presigned URL to a CSV file matching the Panama CSS/SIPE column set:
cedula, nombre_completo, salario_bruto, css_empleado,
seguro_educativo_empleado, isr, css_empleador,
seguro_educativo_empleador, riesgo_profesional
One row per PayrollItem in the run. Surface as a "Download for SIPE" button on the run detail page.
Open follow-ups (not yet implemented)¶
- ISR brackets are annualized inside the calculator — biweekly/monthly periods are annualized; year-end true-up isn't implemented.
- No bonuses / overtime input path —
bonusesandovertimeHoursfields exist onPayrollItembutcalculatePayrolldoesn't ingest them; add a "Per-employee adjustments" UI + mutation before relying on them. - No decimo tercer mes accrual — Panama 13th-month bonus isn't computed.
- Tax-rate change: when Panama rates change, add a new strategy class with a
valid_fromrather than editing in place so historical runs stay reproducible.