Compare commits
2 Commits
cfed13c9f5
...
81009be0c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81009be0c6 | ||
|
|
803f8696e7 |
@ -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 |
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user