diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a572a50 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,174 @@ +# 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 17 view functions (~1700 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 +- **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 advances with principal and remaining_balance tracking +- **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']` — increase worker's net pay +- **DEDUCTIVE_TYPES** = `['Deduction', 'Loan Repayment', 'Advance Payment']` — decrease net pay + +## PayrollAdjustment Type Handling +- **Bonus / Deduction** — standalone, require a linked Project +- **New Loan** — creates a `Loan` record; editing syncs loan amount/balance/reason; deleting cascades to Loan + unpaid repayments +- **Overtime** — links to `WorkLog` via `adj.work_log` FK; managed by `price_overtime()` view +- **Loan Repayment** — links to `Loan` via `adj.loan` FK; loan balance changes during payment processing +- **Advance Payment** — requires a linked Project; reduces net pay + +## 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 + +## 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///` | `toggle_active` | Admin: AJAX toggle active status | +| `/payroll/` | `payroll_dashboard` | Admin: pending payments, loans, charts | +| `/payroll/pay//` | `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//edit/` | `edit_adjustment` | Admin: edit unpaid adjustment | +| `/payroll/adjustment//delete/` | `delete_adjustment` | Admin: delete unpaid adjustment | +| `/payroll/preview//` | `preview_payslip` | Admin: AJAX JSON payslip preview | +| `/payroll/payslip//` | `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 | + +## 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**: Run automatically during Flatlogic rebuild +- **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 diff --git a/core/migrations/0003_add_project_start_end_dates.py b/core/migrations/0003_add_project_start_end_dates.py new file mode 100644 index 0000000..5b437e2 --- /dev/null +++ b/core/migrations/0003_add_project_start_end_dates.py @@ -0,0 +1,25 @@ +# Migration to add start_date and end_date to Project. +# This migration was applied to the database during the Flatlogic export +# but the file was missing from the repository. Re-created to match DB state. + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_update_worker_id_numbers'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='end_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='project', + name='start_date', + field=models.DateField(blank=True, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index 31b5a5a..611a3a0 100644 --- a/core/models.py +++ b/core/models.py @@ -27,6 +27,8 @@ class Project(models.Model): description = models.TextField(blank=True) supervisors = models.ManyToManyField(User, related_name='assigned_projects') active = models.BooleanField(default=True) + start_date = models.DateField(blank=True, null=True) + end_date = models.DateField(blank=True, null=True) def __str__(self): return self.name diff --git a/core/templates/core/attendance_log.html b/core/templates/core/attendance_log.html index 10223c0..c1f740e 100644 --- a/core/templates/core/attendance_log.html +++ b/core/templates/core/attendance_log.html @@ -35,18 +35,17 @@
{% csrf_token %} {# Re-submit all form data with a conflict_action flag #} + {# Non-multi-value fields from form.data #} {% for key, value in form.data.items %} - {% if key != 'csrfmiddlewaretoken' and key != 'conflict_action' %} - {% if key == 'workers' %} - {# Workers is a multi-value field — need each value separately #} - {% for worker_val in form.data.workers %} - - {% endfor %} - {% else %} - - {% endif %} + {% if key != 'csrfmiddlewaretoken' and key != 'conflict_action' and key != 'workers' %} + {% endif %} {% endfor %} + {# Workers is a multi-value field — use the explicit list #} + {# passed from the view (QueryDict.getlist) to avoid losing values #} + {% for wid in selected_worker_ids %} + + {% endfor %}