Compare commits

...

2 Commits

Author SHA1 Message Date
Konrad du Plessis
81009be0c6 Add PPE sizing and drivers license fields to Worker model
New fields: shoe_size, overall_top_size, pants_size, tshirt_size,
has_drivers_license (boolean), drivers_license (file upload).
Admin organised into 3 fieldsets. CSV export updated with new columns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:10:46 +02:00
Konrad du Plessis
803f8696e7 Update CLAUDE.md: accurate function count, quick adjust docs
- views.py now has 27 functions (~2470 lines)
- Document the Quick Adjust button on pending payments rows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 12:25:22 +02:00
5 changed files with 82 additions and 4 deletions

View File

@ -26,7 +26,7 @@ core/ — Single main app: ALL business logic, models, views, forms,
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)
views.py — All 27 functions (~2470 lines, includes helpers)
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)
@ -37,7 +37,7 @@ 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
- **Worker** — profiles with salary, `daily_rate` property (monthly_salary / 20), photo, ID doc, PPE sizing (shoe, overall top, pants, tshirt), drivers license (boolean + file upload)
- **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)
@ -107,6 +107,7 @@ python manage.py check # System check
- 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.
- Quick Adjust Button: Each pending payments row has an "Adjust" button (slider icon) that opens the Add Adjustment modal with that worker pre-checked and their most recent project pre-selected. The header "Add Adjustment" button resets the modal to a clean state. Uses `_quickAdjustOpen` flag to distinguish between the two open paths.
## URL Routes
| Path | View | Purpose |

View File

@ -20,9 +20,24 @@ class ProjectAdmin(admin.ModelAdmin):
@admin.register(Worker)
class WorkerAdmin(admin.ModelAdmin):
list_display = ('name', 'id_number', 'monthly_salary', 'active')
list_filter = ('active',)
list_filter = ('active', 'has_drivers_license')
search_fields = ('name', 'id_number', 'phone_number')
# === FIELDSETS ===
# Organise the worker edit form into clear sections
fieldsets = (
('Personal Info', {
'fields': ('name', 'id_number', 'phone_number', 'monthly_salary',
'employment_date', 'active', 'notes'),
}),
('Sizing', {
'fields': ('shoe_size', 'overall_top_size', 'pants_size', 'tshirt_size'),
}),
('Documents & License', {
'fields': ('photo', 'id_document', 'has_drivers_license', 'drivers_license'),
}),
)
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'supervisor', 'pay_frequency', 'pay_start_date', 'active')

View File

@ -0,0 +1,43 @@
# Generated by Django 5.2.7 on 2026-04-20 10:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_team_pay_frequency_team_pay_start_date'),
]
operations = [
migrations.AddField(
model_name='worker',
name='drivers_license',
field=models.FileField(blank=True, null=True, upload_to='workers/documents/'),
),
migrations.AddField(
model_name='worker',
name='has_drivers_license',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='worker',
name='overall_top_size',
field=models.CharField(blank=True, max_length=10),
),
migrations.AddField(
model_name='worker',
name='pants_size',
field=models.CharField(blank=True, max_length=20),
),
migrations.AddField(
model_name='worker',
name='shoe_size',
field=models.CharField(blank=True, max_length=20),
),
migrations.AddField(
model_name='worker',
name='tshirt_size',
field=models.CharField(blank=True, max_length=10),
),
]

View File

@ -44,6 +44,18 @@ class Worker(models.Model):
notes = models.TextField(blank=True)
active = models.BooleanField(default=True)
# === SIZING ===
# Clothing and boot sizes for PPE (personal protective equipment) ordering
shoe_size = models.CharField(max_length=20, blank=True)
overall_top_size = models.CharField(max_length=10, blank=True)
pants_size = models.CharField(max_length=20, blank=True)
tshirt_size = models.CharField(max_length=10, blank=True)
# === DRIVERS LICENSE ===
# Track which workers have a valid drivers license and store a scanned copy
has_drivers_license = models.BooleanField(default=False)
drivers_license = models.FileField(upload_to='workers/documents/', blank=True, null=True)
@property
def daily_rate(self):
# monthly salary divided by 20 working days

View File

@ -769,7 +769,9 @@ def export_workers_csv(request):
writer = csv.writer(response)
writer.writerow([
'Name', 'ID Number', 'Phone Number', 'Monthly Salary',
'Daily Rate', 'Employment Date', 'Active', 'Notes'
'Daily Rate', 'Employment Date', 'Active', 'Notes',
'Shoe Size', 'Overall Top Size', 'Pants Size', 'T-Shirt Size',
'Has Drivers License',
])
for w in workers:
@ -782,6 +784,11 @@ def export_workers_csv(request):
w.employment_date.strftime('%Y-%m-%d') if w.employment_date else '',
'Yes' if w.active else 'No',
w.notes,
w.shoe_size,
w.overall_top_size,
w.pants_size,
w.tshirt_size,
'Yes' if w.has_drivers_license else 'No',
])
return response