- Add batch pay workflow docs (schedule vs pay-all modes, shared helper) - Add batch-pay preview and process endpoints to URL routes table - Update view count to 19 (~2000 lines) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
14 KiB
FoxFitt LabourPay v5
Coding Style
- Always add clear section header comments using the format: # === SECTION NAME ===
- Add plain English comments explaining what complex logic does
- The project owner is not a programmer — comments should be understandable by a non-technical person
- When creating or editing code, maintain the existing comment structure
Project Overview
Django payroll management system for FoxFitt Construction, a civil works contractor specializing in solar farm foundation installations. Manages field worker attendance, payroll processing, employee loans, and business expenses for solar farm projects.
This is v5 — a fresh export from Flatlogic/AppWizzy, rebuilt from the v2 codebase with simplified models and cleaner structure.
Tech Stack
- Django 5.2.7, Python 3.13, MySQL (production on Flatlogic Cloud Run) / SQLite (local dev)
- Bootstrap 5.3.3 (CDN), Font Awesome 6.5.1 (CDN), Google Fonts (Inter + Poppins)
- xhtml2pdf for PDF generation (payslips, receipts)
- Gmail SMTP for automated document delivery
- Hosted on Flatlogic/AppWizzy platform (Apache on Debian, e2-micro VM)
Project Structure
config/ — Django project settings, URLs, WSGI/ASGI
core/ — Single main app: ALL business logic, models, views, forms, templates
context_processors.py — Injects deployment_timestamp (cache-busting), Flatlogic branding vars
forms.py — AttendanceLogForm, PayrollAdjustmentForm, ExpenseReceiptForm + formset
models.py — All 10 database models
utils.py — render_to_pdf() helper (lazy xhtml2pdf import)
views.py — All 19 view functions (~2000 lines)
management/commands/ — setup_groups, setup_test_data, import_production_data
templates/ — base.html + 7 page templates + 2 email + 2 PDF + login
ai/ — Flatlogic AI proxy client (not used in app logic)
static/css/ — custom.css (CSS variables, component styles)
staticfiles/ — Collected static assets (Bootstrap, admin)
Key Models
- UserProfile — extends Django User (OneToOne); minimal, no extra fields in v5
- Project — work sites with supervisor assignments (M2M User), start/end dates, active flag
- Worker — profiles with salary,
daily_rateproperty (monthly_salary / 20), photo, ID doc - Team — groups of workers under a supervisor, with optional pay schedule (
pay_frequency: weekly/fortnightly/monthly,pay_start_date: anchor date) - WorkLog — daily attendance: date, project, team, workers (M2M), supervisor, overtime,
priced_workers(M2M) - PayrollRecord — completed payments linked to WorkLogs (M2M) and Worker (FK)
- PayrollAdjustment — bonuses, deductions, overtime, loans; linked to project (FK, optional), worker, and optionally to a Loan or WorkLog
- Loan — worker loans AND advances with principal, remaining_balance,
loan_type('loan' or 'advance') - ExpenseReceipt / ExpenseLineItem — business expense records with VAT handling
Key Business Rules
- All business logic lives in the
core/app — do not create additional Django apps - Workers have a
daily_rateproperty:monthly_salary / Decimal('20.00') - Admin =
is_stafforis_superuser(checked viais_admin(user)helper in views.py) - Supervisors see only their assigned projects, teams, and workers
- Admins have full access to payroll, adjustments, and resource management
- WorkLog is the central attendance record — links workers to projects on specific dates
- Attendance logging includes conflict detection (prevents double-logging same worker+date+project)
- Loans have automated repayment deductions during payroll processing
- Cascading deletes use SET_NULL for supervisors/teams to preserve historical data
Payroll Constants
Defined at top of views.py — used in dashboard calculations and payment processing:
- ADDITIVE_TYPES =
['Bonus', 'Overtime', 'New Loan', 'Advance Payment']— increase worker's net pay - DEDUCTIVE_TYPES =
['Deduction', 'Loan Repayment', 'Advance Repayment']— decrease net pay
PayrollAdjustment Type Handling
- Bonus / Deduction — standalone, require a linked Project
- New Loan — creates a
Loanrecord (loan_type='loan'); editing syncs loan amount/balance/reason; deleting cascades to Loan + unpaid repayments - Advance Payment — auto-processed immediately (never sits in Pending): creates
Loan(loan_type='advance'), creates PayrollRecord, sends payslip email, and auto-creates an "Advance Repayment" for the next salary. Requires a Project (for cost tracking) and at least one unpaid work log (otherwise use New Loan). - Overtime — links to
WorkLogviaadj.work_logFK; managed byprice_overtime()view - Loan Repayment — links to
Loan(loan_type='loan') viaadj.loanFK; loan balance changes during payment processing - Advance Repayment — auto-created when an advance is paid; deducts from advance balance during
process_payment(). If partial repayment, remaining balance converts advance to regular loan (loan_typechanges from 'advance' to 'loan'). Editable by admin (amount can be reduced before payday).
Outstanding Payments Logic (Dashboard)
The dashboard's outstanding amount uses per-worker checking, not per-log:
- For each WorkLog, get the set of
paid_worker_idsfrom linked PayrollRecords - A worker is "unpaid for this log" only if their ID is NOT in that set
- This correctly handles partially-paid logs (e.g., one worker paid, another not)
- Unpaid adjustments: additive types increase outstanding, deductive types decrease it
Commands
# Local development (SQLite)
set USE_SQLITE=true && python manage.py runserver 0.0.0.0:8000
# Or use run_dev.bat which sets the env var
python manage.py migrate # Apply database migrations
python manage.py setup_groups # Create Admin + Work Logger permission groups
python manage.py setup_test_data # Populate sample workers, projects, logs
python manage.py import_production_data # Import real production data (14 workers)
python manage.py collectstatic # Collect static files for production
python manage.py check # System check
Development Workflow
- Active development branch:
ai-dev(PR target:master) - Local dev uses SQLite: set
USE_SQLITE=trueenvironment variable - Preview server config:
.claude/launch.json→ runsrun_dev.bat - Admin check in views:
is_admin(request.user)helper (top of views.py) - "Unpaid" adjustment =
payroll_record__isnull=True(no linked PayrollRecord) - POST-only mutation views pattern: check
request.method != 'POST'→ redirect - Template UI: Bootstrap 5 modals with dynamic JS, data-* attributes on clickable elements
- Atomic transactions:
process_payment()usesselect_for_update()to prevent duplicate payments - Payslip Preview modal ("Worker Payment Hub"): shows earnings, adjustments, net pay, plus active loans/advances with inline repayment forms. Uses
refreshPreview()JS function that re-fetches after AJAX repayment submission. Repayment POSTs toadd_repayment_ajaxwhich creates a PayrollAdjustment (balance deduction only happens duringprocess_payment()) - Advance Payment auto-processing:
add_adjustmentimmediately creates PayrollRecord + sends payslip when an advance is created. Also auto-creates an "Advance Repayment" adjustment for the next salary cycle. Uses_send_payslip_email()helper (shared withprocess_payment) - Advance-to-loan conversion: When an Advance Repayment is only partially paid,
process_paymentchanges the Loan'sloan_typefrom 'advance' to 'loan' so the remainder is tracked as a regular loan - Split Payslip: Preview modal has checkboxes on work logs and adjustments (all checked by default).
process_payment()accepts optionalselected_log_ids/selected_adj_idsPOST params to pay only selected items. Falls back to "pay all" if no IDs provided (backward compatible with the quick Pay button). - Team Pay Schedules: Teams have optional
pay_frequency+pay_start_datefields.get_pay_period(team)calculates current period boundaries by stepping forward from the anchor date. The preview modal shows a "Split at Pay Date" button that auto-unchecks items outside the current pay period.get_worker_active_team(worker)returns the worker's first active team. - Pay period calculation:
pay_start_dateis an anchor (never needs updating). Weekly=7 days, Fortnightly=14 days, Monthly=calendar month stepping. Usescalendar.monthrange()for month-length edge cases (nodateutildependency). - Batch Pay: "Batch Pay" button on payroll dashboard opens a modal with two radio modes — "Until Last Paydate" (default, splits at last completed pay period per team schedule) and "Pay All" (includes all unpaid items regardless of date). Preview fetches from
batch_pay_previewwith?mode=schedule|all. Workers without team pay schedules are skipped in schedule mode but included in Pay All mode.batch_payPOST endpoint processes each worker in independent atomic transactions; emails are sent after all payments complete. Uses_process_single_payment()shared helper (same logic as individualprocess_payment).
URL Routes
| Path | View | Purpose |
|---|---|---|
/ |
index |
Dashboard (admin stats / supervisor work view) |
/attendance/log/ |
attendance_log |
Log daily work with date range support |
/history/ |
work_history |
Work logs table with filters |
/history/export/ |
export_work_log_csv |
Download filtered logs as CSV |
/workers/export/ |
export_workers_csv |
Admin: export all workers to CSV |
/toggle/<model>/<id>/ |
toggle_active |
Admin: AJAX toggle active status |
/payroll/ |
payroll_dashboard |
Admin: pending payments, loans, charts |
/payroll/pay/<worker_id>/ |
process_payment |
Admin: process payment (atomic) |
/payroll/price-overtime/ |
price_overtime |
Admin: AJAX price unpriced OT entries |
/payroll/adjustment/add/ |
add_adjustment |
Admin: create adjustment |
/payroll/adjustment/<id>/edit/ |
edit_adjustment |
Admin: edit unpaid adjustment |
/payroll/adjustment/<id>/delete/ |
delete_adjustment |
Admin: delete unpaid adjustment |
/payroll/preview/<worker_id>/ |
preview_payslip |
Admin: AJAX JSON payslip preview (includes active loans) |
/payroll/repayment/<worker_id>/ |
add_repayment_ajax |
Admin: AJAX add loan/advance repayment from preview |
/payroll/payslip/<pk>/ |
payslip_detail |
Admin: view completed payslip |
/receipts/create/ |
create_receipt |
Staff: expense receipt with line items |
/import-data/ |
import_data |
Setup: run import command from browser |
/payroll/batch-pay/preview/ |
batch_pay_preview |
Admin: AJAX JSON batch pay preview (?mode=schedule|all) |
/payroll/batch-pay/ |
batch_pay |
Admin: POST process batch payments for multiple workers |
/run-migrate/ |
run_migrate |
Setup: run pending DB migrations from browser |
Frontend Design Conventions
- CSS variables in
static/css/custom.css:root— always usevar(--name):--primary-dark: #0f172a(navbar),--primary: #1e293b(headers),--accent: #10b981(brand green)--text-main: #334155,--text-secondary: #64748b,--background: #f1f5f9
- Icons: Font Awesome 6 only (
fas fa-*). Do NOT use Bootstrap Icons (bi bi-*) - CTA buttons:
btn-accent(green) for primary actions.btn-primary(dark slate) for modal Save/Submit - Page titles:
{% block title %}Page Name | Fox Fitt{% endblock %} - Fonts: Inter (body) + Poppins (headings) loaded in base.html via Google Fonts CDN
- Cards: Borderless with
box-shadow: 0 4px 6px rgba(0,0,0,0.1). Stat cards usebackdrop-filter: blur - Email/PDF payslips: Worker name dominant — do NOT add prominent FoxFitt branding
Permission Groups
Created by setup_groups management command:
- Admin — full CRUD on all core models
- Work Logger — add/change/view WorkLog; view-only on Project/Worker/Team
Authentication
- Django's built-in auth (
django.contrib.auth) - Login:
/accounts/login/→ redirects to/(home) - Logout: POST to
/accounts/logout/→ redirects to login - All views use
@login_requiredexceptimport_data() - No PIN auth in v5 (simplified from v2)
Environment Variables
DJANGO_SECRET_KEY, DJANGO_DEBUG, HOST_FQDN, CSRF_TRUSTED_ORIGIN
DB_NAME, DB_USER, DB_PASS, DB_HOST (default: 127.0.0.1), DB_PORT (default: 3306)
USE_SQLITE # "true" → use SQLite instead of MySQL
EMAIL_HOST_USER, EMAIL_HOST_PASSWORD (Gmail App Password — 16 chars)
DEFAULT_FROM_EMAIL, SPARK_RECEIPT_EMAIL
PROJECT_DESCRIPTION, PROJECT_IMAGE_URL # Flatlogic branding
Flatlogic/AppWizzy Deployment
- Branches:
ai-dev= development (Flatlogic AI + Claude Code).master= deploy target. - Workflow: Push to
ai-dev→ Flatlogic auto-detects → "Pull Latest" → app rebuilds (~5 min) - Deploy from Git (Settings): Full rebuild from
master— use for production - Migrations: Sometimes run automatically during rebuild, but NOT always reliable. If you get "Unknown column" errors after pulling latest, visit
/run-migrate/in the browser to apply pending migrations manually. This endpoint runspython manage.py migrateon the production MySQL database. - Never edit
ai-devdirectly on GitHub — Flatlogic pushes overwrite it - Gemini gotcha: Flatlogic's Gemini AI reads
__pycache__/*.pycand gets confused. Tell it: "Do NOT read .pyc files. Only work with .py source files." - Sequential workflow: Don't edit in Flatlogic and Claude Code at the same time
Security Notes
- Production:
SESSION_COOKIE_SECURE=True,CSRF_COOKIE_SECURE=True,SameSite=None(cross-origin for Flatlogic iframe) - Local dev: Secure cookies disabled when
USE_SQLITE=true - X-Frame-Options middleware disabled (required for Flatlogic preview)
- Email App Password should be in env var, not hardcoded in settings.py
Important Context
- The owner (Konrad) is not a developer — explain changes clearly and avoid unnecessary complexity
- This system handles real payroll for field workers — accuracy is critical
render_to_pdf()uses lazy import of xhtml2pdf to prevent app crash if library missing- Django admin is available at
/admin/with full model registration and search/filter