Complete labour lifecycle — Attendance → Overtime → Payroll → Payslip → GL posting
InfraTraq implements the complete Labour & Payroll cycle: Worker Registration → Attendance → Overtime → Payroll Run → Payslip → GL Posting. The module spans 14 labour tables and 7 payroll tables (21 total), ensuring full Indian construction compliance including PF, ESI, Professional Tax, bonus, and minimum wages per state.
| Status | Description | Allowed Actions | Next States |
|---|---|---|---|
| Draft | Payroll run created but not yet computed | Edit, Compute Payroll, Delete | Computed |
| Computed | Gross/deductions calculated, pending review | Review, Recalculate, Submit | Pending Approval |
| Pending Approval | Awaiting finance manager / project head approval | Approve, Reject, Return | Approved, Rejected |
| Approved | Payroll approved, ready for bank file generation | Generate Bank File, Generate Payslips | Disbursed |
| Disbursed | Salary transferred to worker bank accounts | Post to GL, View Payslips | Posted |
| Posted | GL journal entries created; payroll finalised | View, Audit, Generate Muster Roll | — |
| Rejected | Approval denied; returned for correction | Edit, Recompute, Resubmit | Draft |
| Cancelled | Payroll run voided before disbursement | View, Audit | — |
worker_id — PK, worker master recordtrade — Worker trade (mason, carpenter, fitter, etc.)skill_level — Unskilled, Semi-skilled, Skilled, Highly Skilleddaily_wage — Daily wage rate (must meet state minimum wage)aadhaar_no — Aadhaar number for statutory identificationpf_number — PF/UAN number for provident fundattendance_id — PK, daily attendance recordworker_id — FK → labour.workershift_id — FK → labour.shiftattendance_date — Date of attendancecheck_in, check_out — Biometric/manual timestampshours_worked — Standard hours (capped at shift duration)ot_hours — Overtime hours (excess over shift)shift_id — PK, shift definitionstart_time, end_time — Shift start and end timesot_multiplier — OT rate multiplier (typically 1.5x or 2x)payroll_id — PK, monthly payroll batch headerproject_id — FK → project.projectperiod_month — Payroll period (YYYY-MM)total_gross — Sum of all worker gross paytotal_deductions — Sum of all statutory + other deductionstotal_net — Net payable amountstatus — draft, computed, approved, disbursed, postedid — PK, per-worker payroll linepayroll_id — FK → payroll.payroll_runworker_id — FK → labour.workerdays_worked, basic_pay, ot_amount, gross_paypf_employee, esi_employee, tds — Statutory deductionsnet_pay — Final disbursable amountstructure_id — PK, wage component definitionbasic_percent — Basic wage percentageda_percent — Dearness Allowance percentagehra_percent — House Rent Allowance percentagepf_percent — PF contribution percentage (default 12%)esi_percent — ESI contribution percentage (default 0.75%)muster_id — PK, monthly muster roll summaryperiod_month — Reporting periodtotal_workers — Headcount for the periodtotal_wages — Total wages paidpf_contribution — Total PF (employee + employer)payslip_id — PK, generated payslip recordpayroll_detail_id — FK → payroll.payroll_detailnet_pay — Net pay amount on payslippdf_url — Download link for payslip PDFRegister worker in labour.worker with trade, skill level, daily wage, Aadhaar number, and PF/ESI numbers. Link to project and assign default shift. Verify minimum wage compliance per state notification.
Record attendance in labour.attendance via biometric device or manual entry. Capture check_in, check_out timestamps. System auto-calculates hours_worked from shift mapping. Gate pass validation ensures worker is authorized for site entry.
Map each attendance record to labour.shift to determine standard hours. Hours exceeding shift duration are flagged as ot_hours. OT requires supervisor approval. OT amount = ot_hours × (daily_wage / shift_hours) × ot_multiplier.
Create payroll.payroll_run for the project/month. For each worker, compute: basic_pay = days_worked × daily_wage. Apply payroll.wage_structure to split into Basic + DA + HRA components. Add OT amount to arrive at gross_pay.
Calculate PF employee share (12% of Basic, capped at ₹15,000 basic), ESI employee share (0.75% of Gross, applicable if gross ≤ ₹21,000), Professional Tax per state schedule, and TDS where applicable. Employer PF (12%) and ESI (3.25%) computed separately as expense.
Deduct outstanding advance/loan EMIs from gross pay. Recovery amount cannot exceed 50% of net pay after statutory deductions. Maintain running balance in worker advance ledger.
Compute net_pay = gross_pay − pf_employee − esi_employee − pt − tds − advance_recovery. Generate bank transfer file (NEFT/RTGS format) for bulk disbursement. Create payroll.payslip with PDF for each worker.
Post payroll journal: Dr Labour Expense (by cost centre), Cr Bank, PF Payable, ESI Payable, TDS Payable. Generate labour.muster_roll as statutory compliance record with total workers, wages, and PF contributions for the period.
class PayrollService { /** Process daily attendance — validate shift and compute hours */ async processAttendance(projectId, date) { const records = await Attendance.findAll({ project_id: projectId, attendance_date: date }); for (const rec of records) { const shift = await Shift.findById(rec.shift_id); const shiftHours = (shift.end_time - shift.start_time) / 3600000; const worked = (rec.check_out - rec.check_in) / 3600000; await rec.update({ hours_worked: Math.min(worked, shiftHours), ot_hours: Math.max(0, worked - shiftHours) }); } } /** Calculate overtime amount for a worker in a given month */ async calculateOT(workerId, month) { const worker = await Worker.findById(workerId); const attendance = await Attendance.findAll({ worker_id: workerId, period: month, ot_hours: { gt: 0 } }); const shift = await Shift.findById(attendance[0].shift_id); const shiftHrs = (shift.end_time - shift.start_time) / 3600000; const hourlyRate = worker.daily_wage / shiftHrs; const totalOTHours = attendance.reduce((s, a) => s + a.ot_hours, 0); return totalOTHours * hourlyRate * shift.ot_multiplier; } /** Run monthly payroll for a project — creates payroll_run + details */ async runPayroll(projectId, month, year) { const workers = await Worker.findAll({ project_id: projectId, status: 'active' }); const run = await PayrollRun.create({ project_id: projectId, period_month: `${year}-${month}`, status: 'draft' }); let totalGross = 0, totalDeductions = 0, totalNet = 0; for (const w of workers) { const daysWorked = await Attendance.count({ worker_id: w.worker_id, period: month }); const basicPay = daysWorked * w.daily_wage; const otAmount = await this.calculateOT(w.worker_id, month); const grossPay = basicPay + otAmount; const deductions = await this.calculateStatutoryDeductions(grossPay, w); const netPay = grossPay - deductions.total; await PayrollDetail.create({ payroll_id: run.payroll_id, worker_id: w.worker_id, days_worked: daysWorked, basic_pay: basicPay, ot_amount: otAmount, gross_pay: grossPay, pf_employee: deductions.pf, esi_employee: deductions.esi, tds: deductions.tds, net_pay: netPay }); totalGross += grossPay; totalDeductions += deductions.total; totalNet += netPay; } await run.update({ total_gross: totalGross, total_deductions: totalDeductions, total_net: totalNet }); return run; } /** Calculate PF, ESI, PT, TDS deductions based on worker profile */ async calculateStatutoryDeductions(grossPay, workerProfile) { const structure = await WageStructure.findById(workerProfile.structure_id); const basicComponent = grossPay * (structure.basic_percent / 100); const pfBasic = Math.min(basicComponent, 15000); // PF ceiling ₹15,000 const pf = pfBasic * 0.12; // Employee PF 12% const esi = grossPay <= 21000 ? grossPay * 0.0075 : 0; // ESI if gross ≤ ₹21,000 const pt = await ProfessionalTax.calculate(workerProfile.state, grossPay); const tds = await TDSCalculator.compute(grossPay * 12, workerProfile); return { pf, esi, pt, tds, total: pf + esi + pt + tds }; } /** Generate NEFT/RTGS bank file for bulk salary disbursement */ async generateBankFile(payrollId) { const details = await PayrollDetail.findAll({ payroll_id: payrollId }); const rows = []; for (const d of details) { const worker = await Worker.findById(d.worker_id); rows.push({ ifsc: worker.bank_ifsc, account: worker.bank_account, name: worker.name, amount: d.net_pay }); } return BankFileGenerator.createNEFT(rows); } /** Post payroll journal entry to GL */ async postToGL(payrollId) { const run = await PayrollRun.findById(payrollId); const je = await JournalEntry.create({ je_type: 'PAYROLL', source_module: 'payroll', total_debit: run.total_gross, total_credit: run.total_gross }); // Dr: Labour Expense (by cost centre) await JournalLine.create({ je_id: je.je_id, account: 'LABOUR_EXPENSE', debit: run.total_gross }); // Cr: Bank, PF Payable, ESI Payable, TDS Payable await JournalLine.create({ je_id: je.je_id, account: 'BANK', credit: run.total_net }); await JournalLine.create({ je_id: je.je_id, account: 'PF_PAYABLE', credit: run.total_pf }); await JournalLine.create({ je_id: je.je_id, account: 'ESI_PAYABLE', credit: run.total_esi }); await JournalLine.create({ je_id: je.je_id, account: 'TDS_PAYABLE', credit: run.total_tds }); return je; } /** Generate muster roll for statutory compliance */ async generateMusterRoll(projectId, month) { const details = await PayrollDetail.findAll({ project_id: projectId, period: month }); const totalWorkers = details.length; const totalWages = details.reduce((s, d) => s + d.gross_pay, 0); const pfContrib = details.reduce((s, d) => s + d.pf_employee, 0) * 2; // Employee + Employer return await MusterRoll.create({ period_month: month, total_workers: totalWorkers, total_wages: totalWages, pf_contribution: pfContrib }); } }
| Rule | Condition | Action |
|---|---|---|
| Minimum Wage Compliance | Daily wage < state minimum wage notification | Block worker registration, alert HR |
| PF Ceiling | Basic component > ₹15,000 | Cap PF computation at ₹15,000 basic |
| ESI Ceiling | Gross pay > ₹21,000/month | Exempt worker from ESI deduction |
| Attendance Hours Cap | hours_worked > shift duration | Cap regular hours at shift limit, excess to OT |
| OT Approval Required | ot_hours > 0 without supervisor approval | Flag OT as pending, exclude from payroll until approved |
| Advance Recovery Limit | Recovery amount > 50% of net pay | Cap recovery at 50%, carry balance to next month |
| Duplicate Payroll Check | Payroll run already exists for same project + period | Block creation, show existing payroll run |
| Event | Source Table | Auto Action |
|---|---|---|
| Attendance Marked | labour.attendance | Update muster summary, compute hours_worked and ot_hours |
| Payroll Approved | payroll.payroll_run | Generate payslips (PDF) + bank transfer file (NEFT/RTGS) |
| Payroll Posted | payroll.payroll_run | Create JE: Dr Labour Expense, Cr Bank/PF Payable/ESI Payable |
| Worker Exit | labour.worker | Trigger final settlement calculation (pending wages + leave encashment + bonus) |
| PF/ESI Due Date | System calendar | Filing reminder notification to HR and Finance (PF by 15th, ESI by 15th) |