Add minimal section headers to all core/ files and CLAUDE.md coding style

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-02-20 00:07:26 +02:00
parent a79cb0b435
commit 370b0e9815
8 changed files with 130 additions and 30 deletions

View File

@ -1,5 +1,11 @@
# Fox Fitt LabourPay # Fox Fitt LabourPay
## 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 ## Project Overview
Django payroll management system for Fox Fitt 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. Django payroll management system for Fox Fitt 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.

View File

@ -5,14 +5,14 @@ from django.db import models
from .models import Worker, Project, Team, WorkLog, UserProfile from .models import Worker, Project, Team, WorkLog, UserProfile
# Inline UserProfile on User admin # === USER ADMIN ===
class UserProfileInline(admin.StackedInline): class UserProfileInline(admin.StackedInline):
model = UserProfile model = UserProfile
can_delete = False can_delete = False
verbose_name_plural = 'Profile' verbose_name_plural = 'Profile'
# Customise the User admin to show groups prominently
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
inlines = [UserProfileInline] inlines = [UserProfileInline]
list_display = ('username', 'first_name', 'last_name', 'is_staff', 'get_groups') list_display = ('username', 'first_name', 'last_name', 'is_staff', 'get_groups')
@ -42,11 +42,12 @@ class UserAdmin(BaseUserAdmin):
get_groups.short_description = 'Groups' get_groups.short_description = 'Groups'
# Re-register User with our custom admin
admin.site.unregister(User) admin.site.unregister(User)
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
# === WORKER ADMIN ===
@admin.register(Worker) @admin.register(Worker)
class WorkerAdmin(admin.ModelAdmin): class WorkerAdmin(admin.ModelAdmin):
list_display = ('name', 'id_no', 'phone_no', 'monthly_salary', 'date_of_employment', 'projects_worked_on_count') list_display = ('name', 'id_no', 'phone_no', 'monthly_salary', 'date_of_employment', 'projects_worked_on_count')
@ -54,6 +55,8 @@ class WorkerAdmin(admin.ModelAdmin):
readonly_fields = ('projects_worked_on_count',) readonly_fields = ('projects_worked_on_count',)
# === PROJECT ADMIN ===
@admin.register(Project) @admin.register(Project)
class ProjectAdmin(admin.ModelAdmin): class ProjectAdmin(admin.ModelAdmin):
list_display = ('name', 'get_supervisors', 'is_active', 'created_at') list_display = ('name', 'get_supervisors', 'is_active', 'created_at')
@ -74,6 +77,8 @@ class ProjectAdmin(admin.ModelAdmin):
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
# === TEAM ADMIN ===
@admin.register(Team) @admin.register(Team)
class TeamAdmin(admin.ModelAdmin): class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'supervisor', 'worker_count', 'is_active', 'created_at') list_display = ('name', 'supervisor', 'worker_count', 'is_active', 'created_at')
@ -94,6 +99,8 @@ class TeamAdmin(admin.ModelAdmin):
return super().formfield_for_foreignkey(db_field, request, **kwargs) return super().formfield_for_foreignkey(db_field, request, **kwargs)
# === WORK LOG ADMIN ===
@admin.register(WorkLog) @admin.register(WorkLog)
class WorkLogAdmin(admin.ModelAdmin): class WorkLogAdmin(admin.ModelAdmin):
list_display = ('date', 'project', 'supervisor') list_display = ('date', 'project', 'supervisor')

View File

@ -1,6 +1,9 @@
import os import os
import time import time
# === TEMPLATE CONTEXT PROCESSORS ===
def project_context(request): def project_context(request):
""" """
Adds project-specific environment variables to the template context globally. Adds project-specific environment variables to the template context globally.

View File

@ -2,6 +2,8 @@ from django import forms
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from .models import WorkLog, Project, Worker, Team, ExpenseReceipt, ExpenseLineItem from .models import WorkLog, Project, Worker, Team, ExpenseReceipt, ExpenseLineItem
# === WORK LOG FORM ===
class WorkLogForm(forms.ModelForm): class WorkLogForm(forms.ModelForm):
end_date = forms.DateField( end_date = forms.DateField(
required=False, required=False,
@ -22,7 +24,6 @@ class WorkLogForm(forms.ModelForm):
) )
team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False, empty_label="Select Team", widget=forms.Select(attrs={'class': 'form-control'})) team = forms.ModelChoiceField(queryset=Team.objects.none(), required=False, empty_label="Select Team", widget=forms.Select(attrs={'class': 'form-control'}))
# Explicitly defining overtime to ensure it's not required and has correct widget
overtime = forms.ChoiceField( overtime = forms.ChoiceField(
choices=WorkLog.OT_CHOICES, choices=WorkLog.OT_CHOICES,
required=False, required=False,
@ -44,29 +45,25 @@ class WorkLogForm(forms.ModelForm):
user = kwargs.pop('user', None) user = kwargs.pop('user', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Base querysets with active filter
projects_qs = Project.objects.filter(is_active=True) projects_qs = Project.objects.filter(is_active=True)
workers_qs = Worker.objects.filter(is_active=True) workers_qs = Worker.objects.filter(is_active=True)
teams_qs = Team.objects.filter(is_active=True) teams_qs = Team.objects.filter(is_active=True)
if user and not user.is_superuser: if user and not user.is_superuser:
# Filter projects and workers based on user assignment
self.fields['project'].queryset = projects_qs.filter(supervisors=user) self.fields['project'].queryset = projects_qs.filter(supervisors=user)
# For workers, we might want to show workers from teams supervised by the user
# OR just all active workers if that's the business rule.
# The previous code filtered workers by managed teams. Let's keep that logic but respecting is_active.
managed_teams = user.managed_teams.all() managed_teams = user.managed_teams.all()
worker_ids = managed_teams.values_list('workers__id', flat=True).distinct() worker_ids = managed_teams.values_list('workers__id', flat=True).distinct()
self.fields['workers'].queryset = workers_qs.filter(id__in=worker_ids) self.fields['workers'].queryset = workers_qs.filter(id__in=worker_ids)
# Filter teams
self.fields['team'].queryset = teams_qs.filter(supervisor=user) self.fields['team'].queryset = teams_qs.filter(supervisor=user)
else: else:
self.fields['project'].queryset = projects_qs self.fields['project'].queryset = projects_qs
self.fields['workers'].queryset = workers_qs self.fields['workers'].queryset = workers_qs
self.fields['team'].queryset = teams_qs self.fields['team'].queryset = teams_qs
# === EXPENSE RECEIPT FORM ===
class ExpenseReceiptForm(forms.ModelForm): class ExpenseReceiptForm(forms.ModelForm):
class Meta: class Meta:
model = ExpenseReceipt model = ExpenseReceipt
@ -79,6 +76,8 @@ class ExpenseReceiptForm(forms.ModelForm):
'vat_type': forms.RadioSelect(attrs={'class': 'form-check-input'}), 'vat_type': forms.RadioSelect(attrs={'class': 'form-check-input'}),
} }
# === EXPENSE LINE ITEM FORMSET ===
ExpenseLineItemFormSet = inlineformset_factory( ExpenseLineItemFormSet = inlineformset_factory(
ExpenseReceipt, ExpenseLineItem, ExpenseReceipt, ExpenseLineItem,
fields=['product', 'amount'], fields=['product', 'amount'],

View File

@ -4,6 +4,9 @@ from decimal import Decimal
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone from django.utils import timezone
# === USER PROFILE ===
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
@ -14,6 +17,9 @@ class UserProfile(models.Model):
def __str__(self): def __str__(self):
return f"{self.user.username}'s profile" return f"{self.user.username}'s profile"
# === PROJECT ===
class Project(models.Model): class Project(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
description = models.TextField(blank=True) description = models.TextField(blank=True)
@ -28,13 +34,15 @@ class Project(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
# === WORKER ===
class Worker(models.Model): class Worker(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
id_no = models.CharField(max_length=50, unique=True, verbose_name="ID Number") id_no = models.CharField(max_length=50, unique=True, verbose_name="ID Number")
phone_no = models.CharField(max_length=20, verbose_name="Phone Number") phone_no = models.CharField(max_length=20, verbose_name="Phone Number")
monthly_salary = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))]) monthly_salary = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0.00'))])
# New fields
photo = models.ImageField(upload_to='workers/photos/', blank=True, null=True) photo = models.ImageField(upload_to='workers/photos/', blank=True, null=True)
id_photo = models.ImageField(upload_to='workers/ids/', blank=True, null=True, verbose_name="ID Document") id_photo = models.ImageField(upload_to='workers/ids/', blank=True, null=True, verbose_name="ID Document")
date_of_employment = models.DateField(default=timezone.now) date_of_employment = models.DateField(default=timezone.now)
@ -53,12 +61,14 @@ class Worker(models.Model):
@property @property
def projects_worked_on_count(self): def projects_worked_on_count(self):
"""Returns the number of distinct projects this worker has worked on."""
return self.work_logs.values('project').distinct().count() return self.work_logs.values('project').distinct().count()
def __str__(self): def __str__(self):
return self.name return self.name
# === TEAM ===
class Team(models.Model): class Team(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
workers = models.ManyToManyField(Worker, related_name='teams') workers = models.ManyToManyField(Worker, related_name='teams')
@ -73,6 +83,9 @@ class Team(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
# === WORK LOG ===
class WorkLog(models.Model): class WorkLog(models.Model):
OT_CHOICES = [ OT_CHOICES = [
(Decimal('0'), 'None'), (Decimal('0'), 'None'),
@ -99,6 +112,9 @@ class WorkLog(models.Model):
def __str__(self): def __str__(self):
return f"{self.date} - {self.project.name}" return f"{self.date} - {self.project.name}"
# === PAYROLL RECORD ===
class PayrollRecord(models.Model): class PayrollRecord(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='payroll_records') worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='payroll_records')
date = models.DateField(default=timezone.now) date = models.DateField(default=timezone.now)
@ -113,6 +129,9 @@ class PayrollRecord(models.Model):
def __str__(self): def __str__(self):
return f"Payment to {self.worker.name} on {self.date}" return f"Payment to {self.worker.name} on {self.date}"
# === LOAN ===
class Loan(models.Model): class Loan(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='loans') worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='loans')
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Principal amount borrowed") amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Principal amount borrowed")
@ -133,6 +152,9 @@ class Loan(models.Model):
def __str__(self): def __str__(self):
return f"Loan for {self.worker.name} - R{self.amount}" return f"Loan for {self.worker.name} - R{self.amount}"
# === PAYROLL ADJUSTMENT ===
class PayrollAdjustment(models.Model): class PayrollAdjustment(models.Model):
ADJUSTMENT_TYPES = [ ADJUSTMENT_TYPES = [
('BONUS', 'Bonus'), ('BONUS', 'Bonus'),
@ -146,7 +168,6 @@ class PayrollAdjustment(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='adjustments') worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='adjustments')
payroll_record = models.ForeignKey(PayrollRecord, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments') payroll_record = models.ForeignKey(PayrollRecord, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments')
loan = models.ForeignKey(Loan, on_delete=models.SET_NULL, null=True, blank=True, related_name='repayments') loan = models.ForeignKey(Loan, on_delete=models.SET_NULL, null=True, blank=True, related_name='repayments')
# Link back to WorkLog to track Project/Team context for Overtime
work_log = models.ForeignKey(WorkLog, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments') work_log = models.ForeignKey(WorkLog, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments')
amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Positive adds to pay, negative subtracts (except for Loan Repayment which is auto-handled)") amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Positive adds to pay, negative subtracts (except for Loan Repayment which is auto-handled)")
@ -161,6 +182,9 @@ class PayrollAdjustment(models.Model):
def __str__(self): def __str__(self):
return f"{self.get_type_display()} - {self.amount} for {self.worker.name}" return f"{self.get_type_display()} - {self.amount} for {self.worker.name}"
# === EXPENSE RECEIPT ===
class ExpenseReceipt(models.Model): class ExpenseReceipt(models.Model):
VAT_CHOICES = [ VAT_CHOICES = [
('INCLUDED', 'VAT Included'), ('INCLUDED', 'VAT Included'),
@ -181,7 +205,6 @@ class ExpenseReceipt(models.Model):
payment_method = models.CharField(max_length=10, choices=PAYMENT_METHODS, default='CASH') payment_method = models.CharField(max_length=10, choices=PAYMENT_METHODS, default='CASH')
vat_type = models.CharField(max_length=10, choices=VAT_CHOICES, default='NONE') vat_type = models.CharField(max_length=10, choices=VAT_CHOICES, default='NONE')
# Financials (Stored for record keeping)
subtotal = models.DecimalField(max_digits=12, decimal_places=2, default=0) subtotal = models.DecimalField(max_digits=12, decimal_places=2, default=0)
vat_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0) vat_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0) total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0)
@ -195,6 +218,9 @@ class ExpenseReceipt(models.Model):
def __str__(self): def __str__(self):
return f"Receipt from {self.vendor} - {self.date}" return f"Receipt from {self.vendor} - {self.date}"
# === EXPENSE LINE ITEM ===
class ExpenseLineItem(models.Model): class ExpenseLineItem(models.Model):
receipt = models.ForeignKey(ExpenseReceipt, on_delete=models.CASCADE, related_name='items') receipt = models.ForeignKey(ExpenseReceipt, on_delete=models.CASCADE, related_name='items')
product = models.CharField(max_length=255, verbose_name="Product/Item") product = models.CharField(max_length=255, verbose_name="Product/Item")

View File

@ -20,21 +20,34 @@ from .views import (
) )
urlpatterns = [ urlpatterns = [
# === HOME & ATTENDANCE ===
path("", home, name="home"), path("", home, name="home"),
path("log-attendance/", log_attendance, name="log_attendance"), path("log-attendance/", log_attendance, name="log_attendance"),
# === WORK LOGS ===
path("work-logs/", work_log_list, name="work_log_list"), path("work-logs/", work_log_list, name="work_log_list"),
path("work-logs/export/", export_work_log_csv, name="export_work_log_csv"), path("work-logs/export/", export_work_log_csv, name="export_work_log_csv"),
# === RESOURCE MANAGEMENT ===
path("manage-resources/", manage_resources, name="manage_resources"), path("manage-resources/", manage_resources, name="manage_resources"),
path("manage-resources/toggle/<str:model_type>/<int:pk>/", toggle_resource_status, name="toggle_resource_status"), path("manage-resources/toggle/<str:model_type>/<int:pk>/", toggle_resource_status, name="toggle_resource_status"),
# === PAYROLL ===
path("payroll/", payroll_dashboard, name="payroll_dashboard"), path("payroll/", payroll_dashboard, name="payroll_dashboard"),
path("payroll/pay/<int:worker_id>/", process_payment, name="process_payment"), path("payroll/pay/<int:worker_id>/", process_payment, name="process_payment"),
path("payroll/preview/<int:worker_id>/", preview_payslip, name="preview_payslip"), path("payroll/preview/<int:worker_id>/", preview_payslip, name="preview_payslip"),
path("payroll/price-overtime/", price_overtime, name="price_overtime"), path("payroll/price-overtime/", price_overtime, name="price_overtime"),
path("payroll/payslip/<int:pk>/", payslip_detail, name="payslip_detail"), path("payroll/payslip/<int:pk>/", payslip_detail, name="payslip_detail"),
# === LOANS ===
path("loans/", loan_list, name="loan_list"), path("loans/", loan_list, name="loan_list"),
path("loans/add/", add_loan, name="add_loan"), path("loans/add/", add_loan, name="add_loan"),
# === ADJUSTMENTS ===
path("payroll/adjustment/add/", add_adjustment, name="add_adjustment"), path("payroll/adjustment/add/", add_adjustment, name="add_adjustment"),
path("payroll/adjustment/<int:pk>/edit/", edit_adjustment, name="edit_adjustment"), path("payroll/adjustment/<int:pk>/edit/", edit_adjustment, name="edit_adjustment"),
path("payroll/adjustment/<int:pk>/delete/", delete_adjustment, name="delete_adjustment"), path("payroll/adjustment/<int:pk>/delete/", delete_adjustment, name="delete_adjustment"),
# === EXPENSES ===
path("receipts/create/", create_receipt, name="create_receipt"), path("receipts/create/", create_receipt, name="create_receipt"),
] ]

View File

@ -4,6 +4,9 @@ from xhtml2pdf import pisa
from django.conf import settings from django.conf import settings
import os import os
# === PDF GENERATION ===
def render_to_pdf(template_src, context_dict={}): def render_to_pdf(template_src, context_dict={}):
template = get_template(template_src) template = get_template(template_src)
html = template.render(context_dict) html = template.render(context_dict)

View File

@ -20,11 +20,15 @@ from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from core.utils import render_to_pdf from core.utils import render_to_pdf
# Adjustment types that ADD to pay
# === CONSTANTS ===
ADDITIVE_TYPES = ['BONUS', 'OVERTIME', 'LOAN'] ADDITIVE_TYPES = ['BONUS', 'OVERTIME', 'LOAN']
# Adjustment types that SUBTRACT from pay
DEDUCTIVE_TYPES = ['DEDUCTION', 'LOAN_REPAYMENT', 'ADVANCE'] DEDUCTIVE_TYPES = ['DEDUCTION', 'LOAN_REPAYMENT', 'ADVANCE']
# === PERMISSION HELPERS ===
def is_admin(user): def is_admin(user):
"""Check if user has admin-level access (staff, superuser, or in Admin group).""" """Check if user has admin-level access (staff, superuser, or in Admin group)."""
return user.is_staff or user.is_superuser return user.is_staff or user.is_superuser
@ -39,6 +43,9 @@ def is_staff_or_supervisor(user):
"""Check if user has at least supervisor-level access.""" """Check if user has at least supervisor-level access."""
return is_admin(user) or is_supervisor(user) return is_admin(user) or is_supervisor(user)
# === HOME DASHBOARD ===
@login_required @login_required
def home(request): def home(request):
"""Render the landing screen with dashboard stats.""" """Render the landing screen with dashboard stats."""
@ -158,6 +165,9 @@ def home(request):
} }
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
# === LOG ATTENDANCE ===
@login_required @login_required
def log_attendance(request): def log_attendance(request):
# Build team workers map for frontend JS (needed for both GET and POST if re-rendering) # Build team workers map for frontend JS (needed for both GET and POST if re-rendering)
@ -307,6 +317,9 @@ def log_attendance(request):
return render(request, 'core/log_attendance.html', context) return render(request, 'core/log_attendance.html', context)
# === WORK LOG LIST ===
@login_required @login_required
def work_log_list(request): def work_log_list(request):
"""View work log history and payroll adjustments with advanced filtering.""" """View work log history and payroll adjustments with advanced filtering."""
@ -547,6 +560,9 @@ def work_log_list(request):
return render(request, 'core/work_log_list.html', context) return render(request, 'core/work_log_list.html', context)
# === EXPORT WORK LOG CSV ===
@login_required @login_required
def export_work_log_csv(request): def export_work_log_csv(request):
"""Export filtered work logs and adjustments to CSV.""" """Export filtered work logs and adjustments to CSV."""
@ -672,6 +688,9 @@ def export_work_log_csv(request):
return response return response
# === RESOURCE MANAGEMENT ===
@login_required @login_required
def manage_resources(request): def manage_resources(request):
"""Redirect to dashboard which now includes manage resources.""" """Redirect to dashboard which now includes manage resources."""
@ -705,6 +724,9 @@ def toggle_resource_status(request, model_type, pk):
return redirect('home') return redirect('home')
# === PAYROLL DASHBOARD ===
@login_required @login_required
def payroll_dashboard(request): def payroll_dashboard(request):
"""Dashboard for payroll management with filtering.""" """Dashboard for payroll management with filtering."""
@ -924,6 +946,9 @@ def payroll_dashboard(request):
} }
return render(request, 'core/payroll_dashboard.html', context) return render(request, 'core/payroll_dashboard.html', context)
# === PROCESS PAYMENT ===
@login_required @login_required
def process_payment(request, worker_id): def process_payment(request, worker_id):
"""Process payment for a worker, mark logs as paid, link adjustments, and email receipt.""" """Process payment for a worker, mark logs as paid, link adjustments, and email receipt."""
@ -1019,6 +1044,8 @@ def process_payment(request, worker_id):
return redirect('payroll_dashboard') return redirect('payroll_dashboard')
# === PREVIEW PAYSLIP ===
@login_required @login_required
def preview_payslip(request, worker_id): def preview_payslip(request, worker_id):
"""Return payslip preview data as JSON (no DB changes, no email).""" """Return payslip preview data as JSON (no DB changes, no email)."""
@ -1062,6 +1089,9 @@ def preview_payslip(request, worker_id):
'net_pay': total_amount, 'net_pay': total_amount,
}) })
# === PRICE OVERTIME ===
@login_required @login_required
def price_overtime(request): def price_overtime(request):
"""Create OVERTIME PayrollAdjustments from unpriced overtime logs.""" """Create OVERTIME PayrollAdjustments from unpriced overtime logs."""
@ -1106,6 +1136,9 @@ def price_overtime(request):
messages.success(request, f"Created {created} overtime adjustment(s).") messages.success(request, f"Created {created} overtime adjustment(s).")
return redirect('payroll_dashboard') return redirect('payroll_dashboard')
# === PAYSLIP DETAIL ===
@login_required @login_required
def payslip_detail(request, pk): def payslip_detail(request, pk):
"""Show details of a payslip (Payment Record).""" """Show details of a payslip (Payment Record)."""
@ -1133,6 +1166,9 @@ def payslip_detail(request, pk):
} }
return render(request, 'core/payslip.html', context) return render(request, 'core/payslip.html', context)
# === LOANS ===
@login_required @login_required
def loan_list(request): def loan_list(request):
"""Redirect to payroll dashboard loans tab.""" """Redirect to payroll dashboard loans tab."""
@ -1162,6 +1198,9 @@ def add_loan(request):
return redirect('/payroll/?status=loans') return redirect('/payroll/?status=loans')
# === PAYROLL ADJUSTMENTS ===
@login_required @login_required
def add_adjustment(request): def add_adjustment(request):
"""Add a payroll adjustment (Bonus, Overtime, Deduction, Repayment) for one or more workers.""" """Add a payroll adjustment (Bonus, Overtime, Deduction, Repayment) for one or more workers."""
@ -1351,6 +1390,7 @@ def edit_adjustment(request, pk):
messages.success(request, f"Updated {adj.get_type_display()} for {adj.worker.name}.") messages.success(request, f"Updated {adj.get_type_display()} for {adj.worker.name}.")
return redirect('payroll_dashboard') return redirect('payroll_dashboard')
@login_required @login_required
def delete_adjustment(request, pk): def delete_adjustment(request, pk):
"""Delete an unpaid payroll adjustment. Admin only, POST only.""" """Delete an unpaid payroll adjustment. Admin only, POST only."""
@ -1394,6 +1434,9 @@ def delete_adjustment(request, pk):
messages.success(request, f"Deleted {type_display} for {worker_name}.") messages.success(request, f"Deleted {type_display} for {worker_name}.")
return redirect('payroll_dashboard') return redirect('payroll_dashboard')
# === EXPENSE RECEIPTS ===
@login_required @login_required
def create_receipt(request): def create_receipt(request):
"""Create a new expense receipt and email it.""" """Create a new expense receipt and email it."""