from django.contrib import admin from .models import ( UserProfile, Project, Worker, Team, WorkLog, PayrollRecord, Loan, PayrollAdjustment, ExpenseReceipt, ExpenseLineItem, WorkerCertificate, WorkerWarning, SiteReport, Absence, ) @admin.register(UserProfile) class UserProfileAdmin(admin.ModelAdmin): list_display = ('user',) search_fields = ('user__username', 'user__first_name', 'user__last_name') @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): list_display = ('name', 'active') list_filter = ('active',) search_fields = ('name', 'description') filter_horizontal = ('supervisors',) # === INLINE ADMINS FOR WORKER === # Let admins manage a worker's certifications and warnings directly # from the Worker change page, without navigating to a separate screen. class WorkerCertificateInline(admin.TabularInline): model = WorkerCertificate extra = 0 # no blank rows by default — admin clicks "Add another" to create readonly_fields = ('created_at',) fields = ('cert_type', 'document', 'issued_date', 'valid_until', 'notes', 'created_at') class WorkerWarningInline(admin.TabularInline): model = WorkerWarning extra = 0 readonly_fields = ('created_at',) fields = ('date', 'severity', 'reason', 'description', 'issued_by', 'document', 'created_at') @admin.register(Worker) class WorkerAdmin(admin.ModelAdmin): list_display = ('name', 'id_number', 'monthly_salary', 'active') list_filter = ('active', 'has_drivers_license') search_fields = ('name', 'id_number', 'phone_number') # Inline sections for certs + warnings appear below the main Worker form inlines = [WorkerCertificateInline, WorkerWarningInline] # === FIELDSETS === # Organise the worker edit form into clear sections. # Banking & Tax fields (UIF, Bank, Acc No.) live inside Personal Info # per product requirement — help_text strings render as hints under # each field in admin (and as tooltips on the friendly edit page). fieldsets = ( ('Personal Info', { 'fields': ('name', 'id_number', 'phone_number', 'monthly_salary', 'tax_number', 'uif_number', 'bank_name', 'bank_account_number', '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', 'drivers_license_code'), }), ) # === STANDALONE ADMINS FOR CERTS + WARNINGS === # Separate pages for bulk operations across workers — "show me all # certs expiring this month" or "show me all final warnings". @admin.register(WorkerCertificate) class WorkerCertificateAdmin(admin.ModelAdmin): list_display = ('worker', 'cert_type', 'issued_date', 'valid_until', 'is_expired') list_filter = ('cert_type',) search_fields = ('worker__name', 'worker__id_number') date_hierarchy = 'valid_until' @admin.register(WorkerWarning) class WorkerWarningAdmin(admin.ModelAdmin): list_display = ('worker', 'date', 'severity', 'reason', 'issued_by') list_filter = ('severity',) search_fields = ('worker__name', 'reason', 'description') date_hierarchy = 'date' @admin.register(Absence) class AbsenceAdmin(admin.ModelAdmin): # `project` shown alongside reason/is_paid so the admin index reads as # "worker — project — reason — paid" at a glance. list_display = ('worker', 'project', 'date', 'reason', 'is_paid', 'logged_by', 'created_at') # `project` filter sits next to reason — handy for "which workers were # absent on Solar Farm Alpha last month". list_filter = ('reason', 'is_paid', 'project', 'date') search_fields = ('worker__name', 'worker__id_number', 'notes') # `project` is a small set normally but raw_id keeps the form fast # even if it grows. Same treatment as the other FKs. raw_id_fields = ('worker', 'logged_by', 'payroll_adjustment', 'project') readonly_fields = ('created_at', 'updated_at') date_hierarchy = 'date' # === KEEP PAYROLL ADJUSTMENT IN SYNC WITH is_paid === # Without this, toggling `is_paid` via /admin/core/absence//change/ # saves the row but does NOT create (or delete) the linked Bonus # PayrollAdjustment — the friendly UI (`absence_edit`) does this # correctly. Mirror that behavior here so the two paths are # consistent. def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) # Import the sync helper inside the method so admin.py doesn't # have to import from views at module load (avoids any chance # of circular-import surprises during Django startup). from .views import _sync_absence_payroll_adjustment try: _sync_absence_payroll_adjustment(obj) except ValueError as e: # The sync helper raises ValueError when the user tries to # un-pay an Absence whose linked adjustment has already been # paid (i.e. a PayrollRecord exists). Surface that to the # admin via the messages framework rather than blowing up. from django.contrib import messages messages.error(request, str(e)) @admin.register(Team) class TeamAdmin(admin.ModelAdmin): list_display = ('name', 'supervisor', 'pay_frequency', 'pay_start_date', 'active') list_editable = ('pay_frequency', 'pay_start_date') list_filter = ('active', 'supervisor', 'pay_frequency') search_fields = ('name',) filter_horizontal = ('workers',) @admin.register(WorkLog) class WorkLogAdmin(admin.ModelAdmin): list_display = ('date', 'project', 'supervisor', 'overtime_amount') list_filter = ('date', 'project', 'supervisor') search_fields = ('project__name', 'notes') filter_horizontal = ('workers', 'priced_workers') @admin.register(PayrollRecord) class PayrollRecordAdmin(admin.ModelAdmin): list_display = ('worker', 'date', 'amount_paid') list_filter = ('date', 'worker') search_fields = ('worker__name',) filter_horizontal = ('work_logs',) @admin.register(Loan) class LoanAdmin(admin.ModelAdmin): list_display = ('worker', 'principal_amount', 'remaining_balance', 'date', 'active') list_filter = ('active', 'date', 'worker') search_fields = ('worker__name', 'reason') @admin.register(SiteReport) class SiteReportAdmin(admin.ModelAdmin): """Admin view for daily site progress reports. `metrics` is a JSONField — Django renders it as a textarea blob, which is fine for low-frequency admin edits. The friendly /site-report//edit/ form is the primary entry point. """ list_display = ('work_log', 'weather', 'temperature_min', 'temperature_max', 'created_by', 'created_at') list_filter = ('weather', 'created_at', 'work_log__project') search_fields = ('notes', 'work_log__project__name', 'work_log__supervisor__username') raw_id_fields = ('work_log', 'created_by') readonly_fields = ('created_at', 'updated_at') @admin.register(PayrollAdjustment) class PayrollAdjustmentAdmin(admin.ModelAdmin): list_display = ('worker', 'type_display', 'amount', 'date') list_filter = ('type', 'date', 'worker') search_fields = ('worker__name', 'description') # === Type column uses the short user-facing label === @admin.display(description='Type', ordering='type') def type_display(self, obj): """Show the short user-facing label (e.g. "Loan", "Advance") instead of the raw DB value ("New Loan", "Advance Payment"). Sorting and filtering still work off the underlying `type` field — this only changes what's printed in the column.""" return obj.get_type_display() class ExpenseLineItemInline(admin.TabularInline): model = ExpenseLineItem extra = 1 @admin.register(ExpenseReceipt) class ExpenseReceiptAdmin(admin.ModelAdmin): list_display = ('vendor_name', 'date', 'total_amount', 'user') list_filter = ('date', 'payment_method', 'vat_type') search_fields = ('vendor_name', 'description') inlines = [ExpenseLineItemInline] @admin.register(ExpenseLineItem) class ExpenseLineItemAdmin(admin.ModelAdmin): list_display = ('product_name', 'amount', 'receipt') search_fields = ('product_name', 'receipt__vendor_name')