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): # === PAY FREQUENCY CHOICES === # Used for the team's recurring pay schedule (weekly, fortnightly, or monthly) PAY_FREQUENCY_CHOICES = [ ('weekly', 'Weekly'), ('fortnightly', 'Fortnightly'), ('monthly', 'Monthly'), ] 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) # === PAY SCHEDULE === # These two fields define when the team gets paid. # pay_start_date is the anchor — the first day of the very first pay period. # pay_frequency determines the length of each recurring period. # Both are optional — teams without a schedule work as before. pay_frequency = models.CharField(max_length=15, choices=PAY_FREQUENCY_CHOICES, blank=True, default='') pay_start_date = models.DateField(blank=True, null=True, help_text='Anchor date for first pay period') 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): # === LOAN TYPE === # 'loan' = traditional loan (created via "New Loan") # 'advance' = salary advance (created via "Advance Payment") # Both work the same way (tracked balance, repayments) but are # labelled differently on payslips and in the Loans tab. LOAN_TYPE_CHOICES = [ ('loan', 'Loan'), ('advance', 'Advance'), ] worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='loans') loan_type = models.CharField(max_length=10, choices=LOAN_TYPE_CHOICES, default='loan') 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): label = 'Advance' if self.loan_type == 'advance' else 'Loan' return f"{self.worker.name} - {label} - {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'), ('Advance Repayment', 'Advance Repayment'), ] 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