38686-vm/core/models.py
Konrad du Plessis 19c662ec7d Fix 3 critical bugs in dashboard + attendance logging
- Fix outstanding payments: check per-worker (not per-log) to handle partially-paid WorkLogs
- Fix adjustment math: deductions now subtract from outstanding instead of adding
- Fix conflict resolution: use explicit worker ID list (QueryDict.getlist) instead of broken form.data.workers iteration
- Add missing migration 0003 for Project start_date/end_date fields
- Add CLAUDE.md project documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:28:11 +02:00

172 lines
7.0 KiB
Python

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from decimal import Decimal
from django.db.models.signals import post_save
from django.dispatch import receiver
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
# Add any extra profile fields if needed in the future
def __str__(self):
return self.user.username
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.get_or_create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
if hasattr(instance, 'profile'):
instance.profile.save()
class Project(models.Model):
name = models.CharField(max_length=200)
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
class Worker(models.Model):
name = models.CharField(max_length=200)
id_number = models.CharField(max_length=50, unique=True)
phone_number = models.CharField(max_length=20, blank=True)
monthly_salary = models.DecimalField(max_digits=10, decimal_places=2)
photo = models.ImageField(upload_to='workers/photos/', blank=True, null=True)
id_document = models.FileField(upload_to='workers/documents/', blank=True, null=True)
employment_date = models.DateField(default=timezone.now)
notes = models.TextField(blank=True)
active = models.BooleanField(default=True)
@property
def daily_rate(self):
# monthly salary divided by 20 working days
return (self.monthly_salary / Decimal('20.00')).quantize(Decimal('0.01'))
def __str__(self):
return self.name
class Team(models.Model):
name = models.CharField(max_length=200)
workers = models.ManyToManyField(Worker, related_name='teams')
supervisor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='supervised_teams')
active = models.BooleanField(default=True)
def __str__(self):
return self.name
class WorkLog(models.Model):
OVERTIME_CHOICES = [
(Decimal('0.00'), 'None'),
(Decimal('0.25'), '1/4 Day'),
(Decimal('0.50'), '1/2 Day'),
(Decimal('0.75'), '3/4 Day'),
(Decimal('1.00'), 'Full Day'),
]
date = models.DateField(default=timezone.now)
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='work_logs')
team = models.ForeignKey(Team, on_delete=models.SET_NULL, null=True, blank=True, related_name='work_logs')
workers = models.ManyToManyField(Worker, related_name='work_logs')
supervisor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='work_logs_created')
notes = models.TextField(blank=True)
overtime_amount = models.DecimalField(max_digits=3, decimal_places=2, choices=OVERTIME_CHOICES, default=Decimal('0.00'))
priced_workers = models.ManyToManyField(Worker, related_name='priced_overtime_logs', blank=True)
@property
def display_amount(self):
"""Total daily cost for all workers on this log (sum of daily_rate).
Works efficiently with prefetch_related('workers')."""
return sum(w.daily_rate for w in self.workers.all())
def __str__(self):
return f"{self.date} - {self.project.name}"
class PayrollRecord(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='payroll_records')
date = models.DateField(default=timezone.now)
amount_paid = models.DecimalField(max_digits=10, decimal_places=2)
work_logs = models.ManyToManyField(WorkLog, related_name='payroll_records')
def __str__(self):
return f"{self.worker.name} - {self.date}"
class Loan(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='loans')
principal_amount = models.DecimalField(max_digits=10, decimal_places=2)
remaining_balance = models.DecimalField(max_digits=10, decimal_places=2)
date = models.DateField(default=timezone.now)
reason = models.TextField(blank=True)
active = models.BooleanField(default=True)
def save(self, *args, **kwargs):
if not self.pk:
self.remaining_balance = self.principal_amount
super().save(*args, **kwargs)
def __str__(self):
return f"{self.worker.name} - Loan - {self.date}"
class PayrollAdjustment(models.Model):
TYPE_CHOICES = [
('Bonus', 'Bonus'),
('Overtime', 'Overtime'),
('Deduction', 'Deduction'),
('Loan Repayment', 'Loan Repayment'),
('New Loan', 'New Loan'),
('Advance Payment', 'Advance Payment'),
]
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')
loan = models.ForeignKey(Loan, on_delete=models.SET_NULL, null=True, blank=True, related_name='repayments')
work_log = models.ForeignKey(WorkLog, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments_by_work_log')
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments_by_project')
amount = models.DecimalField(max_digits=10, decimal_places=2)
date = models.DateField(default=timezone.now)
description = models.TextField(blank=True)
type = models.CharField(max_length=50, choices=TYPE_CHOICES)
def __str__(self):
return f"{self.worker.name} - {self.type} - {self.amount}"
class ExpenseReceipt(models.Model):
METHOD_CHOICES = [
('Cash', 'Cash'),
('Card', 'Card'),
('EFT', 'EFT'),
('Other', 'Other'),
]
VAT_CHOICES = [
('Included', 'Included'),
('Excluded', 'Excluded'),
('None', 'None'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense_receipts')
date = models.DateField(default=timezone.now)
vendor_name = models.CharField(max_length=200)
description = models.TextField(blank=True)
payment_method = models.CharField(max_length=20, choices=METHOD_CHOICES)
vat_type = models.CharField(max_length=20, choices=VAT_CHOICES)
subtotal = models.DecimalField(max_digits=12, decimal_places=2)
vat_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
total_amount = models.DecimalField(max_digits=12, decimal_places=2)
def __str__(self):
return f"{self.vendor_name} - {self.date}"
class ExpenseLineItem(models.Model):
receipt = models.ForeignKey(ExpenseReceipt, on_delete=models.CASCADE, related_name='line_items')
product_name = models.CharField(max_length=200)
amount = models.DecimalField(max_digits=12, decimal_places=2)
def __str__(self):
return self.product_name