188 lines
15 KiB
Markdown
188 lines
15 KiB
Markdown
# 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_rate` property (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_rate` property: `monthly_salary / Decimal('20.00')`
|
|
- Admin = `is_staff` or `is_superuser` (checked via `is_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 `Loan` record (`loan_type='loan'`); has a "Pay Immediately" checkbox (checked by default) that auto-processes the loan (creates PayrollRecord, sends payslip to Spark, marks as paid). When unchecked, the loan sits in Pending Payments for the next pay cycle. 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 `WorkLog` via `adj.work_log` FK; managed by `price_overtime()` view
|
|
- **Loan Repayment** — links to `Loan` (loan_type='loan') via `adj.loan` FK; 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_type` changes 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_ids` from 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
|
|
```bash
|
|
# 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=true` environment variable
|
|
- Preview server config: `.claude/launch.json` → runs `run_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()` uses `select_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 to `add_repayment_ajax` which creates a PayrollAdjustment (balance deduction only happens during `process_payment()`)
|
|
- Advance Payment auto-processing: `add_adjustment` immediately 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 with `process_payment`)
|
|
- Advance-to-loan conversion: When an Advance Repayment is only partially paid, `process_payment` changes the Loan's `loan_type` from '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 optional `selected_log_ids` / `selected_adj_ids` POST 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_date` fields. `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 after the `cutoff_date` (end of last completed period — includes ALL overdue work, not just one period). `get_worker_active_team(worker)` returns the worker's first active team.
|
|
- Pay period calculation: `pay_start_date` is an anchor (never needs updating). Weekly=7 days, Fortnightly=14 days, Monthly=calendar month stepping. Uses `calendar.monthrange()` for month-length edge cases (no `dateutil` dependency).
|
|
- 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_preview` with `?mode=schedule|all`. Workers without team pay schedules are skipped in schedule mode but included in Pay All mode. `batch_pay` POST endpoint processes each worker in independent atomic transactions; emails are sent after all payments complete. Uses `_process_single_payment()` shared helper (same logic as individual `process_payment`). Modal includes team filter dropdown and 3-option loan filter (All / With loans only / Without loans).
|
|
- Pending Payments Table: Shows overdue badges (red) for workers with unpaid work from completed pay periods, and loan badges (yellow) for workers with active loans/advances. Filter bar has: team dropdown, "Overdue only" checkbox, and loan dropdown (All Workers / With loans only / Without loans). Overdue detection uses `get_pay_period()` cutoff logic.
|
|
|
|
## 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 use `var(--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 use `backdrop-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_required` except `import_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 runs `python manage.py migrate` on the production MySQL database.
|
|
- **Never edit `ai-dev` directly on GitHub** — Flatlogic pushes overwrite it
|
|
- **Gemini gotcha**: Flatlogic's Gemini AI reads `__pycache__/*.pyc` and 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
|