diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 0b0b86d..e842ed4 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 6cc1e74..b622d85 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0015_payrolladjustment_work_log.py b/core/migrations/0015_payrolladjustment_work_log.py new file mode 100644 index 0000000..30cb4f8 --- /dev/null +++ b/core/migrations/0015_payrolladjustment_work_log.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2026-02-10 11:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0014_remove_worklog_overtime_priced_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='payrolladjustment', + name='work_log', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments', to='core.worklog'), + ), + ] diff --git a/core/migrations/__pycache__/0015_payrolladjustment_work_log.cpython-311.pyc b/core/migrations/__pycache__/0015_payrolladjustment_work_log.cpython-311.pyc new file mode 100644 index 0000000..69b31b7 Binary files /dev/null and b/core/migrations/__pycache__/0015_payrolladjustment_work_log.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 96b9e07..7c3d0ee 100644 --- a/core/models.py +++ b/core/models.py @@ -145,6 +145,9 @@ class PayrollAdjustment(models.Model): 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') + # 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') + 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)") date = models.DateField(default=timezone.now) description = models.CharField(max_length=255) @@ -201,4 +204,4 @@ class ExpenseLineItem(models.Model): verbose_name_plural = "Expense Line Items" def __str__(self): - return f"{self.product} - {self.amount}" \ No newline at end of file + return f"{self.product} - {self.amount}" diff --git a/core/views.py b/core/views.py index b7b3dd9..cb7ca5d 100644 --- a/core/views.py +++ b/core/views.py @@ -63,10 +63,23 @@ def home(request): if user_is_admin: # 1. Outstanding Payments - active_workers = Worker.objects.filter(is_active=True) + active_workers = Worker.objects.filter(is_active=True).prefetch_related('work_logs', 'adjustments') for worker in active_workers: + # Unpaid logs unpaid_logs_count = worker.work_logs.exclude(paid_in__worker=worker).count() - outstanding_total += unpaid_logs_count * worker.day_rate + log_amount = unpaid_logs_count * worker.day_rate + + # Pending Adjustments + pending_adjustments = worker.adjustments.filter(payroll_record__isnull=True) + adj_total = Decimal('0.00') + for adj in pending_adjustments: + if adj.type in ['BONUS', 'OVERTIME', 'LOAN']: + adj_total += adj.amount + elif adj.type in ['DEDUCTION', 'LOAN_REPAYMENT']: + adj_total -= adj.amount + + total_payable = log_amount + adj_total + outstanding_total += max(total_payable, Decimal('0.00')) # 2. Paid This Month recent_payments_total = PayrollRecord.objects.filter( @@ -298,22 +311,22 @@ def work_log_list(request): logs = logs.filter(paid_in__isnull=True) # --- 2. Fetch Adjustments --- - # Adjustments are shown unless a Project/Team filter is active (as they don't belong to projects/teams), - # OR if a specific worker is selected (then we always show their adjustments). - show_adjustments = True - if (project_id or team_id) and not worker_id: - show_adjustments = False + adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log') - adjustments = PayrollAdjustment.objects.none() - if show_adjustments: - adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record') - if worker_id: - adjustments = adjustments.filter(worker_id=worker_id) + if worker_id: + adjustments = adjustments.filter(worker_id=worker_id) + + if project_id: + # Include only adjustments linked to this project (via work_log) + adjustments = adjustments.filter(work_log__project_id=project_id) - if payment_status == 'paid': - adjustments = adjustments.filter(payroll_record__isnull=False) - elif payment_status == 'unpaid': - adjustments = adjustments.filter(payroll_record__isnull=True) + if team_id: + adjustments = adjustments.filter(work_log__team_id=team_id) + + if payment_status == 'paid': + adjustments = adjustments.filter(payroll_record__isnull=False) + elif payment_status == 'unpaid': + adjustments = adjustments.filter(payroll_record__isnull=True) # --- 3. Date Filtering for Calendar View (Applied to both) --- start_date = None @@ -338,8 +351,8 @@ def work_log_list(request): end_date = datetime.date(curr_year, curr_month, num_days) logs = logs.filter(date__range=(start_date, end_date)) - if show_adjustments: - adjustments = adjustments.filter(date__range=(start_date, end_date)) + # No 'show_adjustments' check needed as query is already filtered + adjustments = adjustments.filter(date__range=(start_date, end_date)) # --- 4. Combine and Sort --- user_is_admin = is_admin(request.user) @@ -376,32 +389,31 @@ def work_log_list(request): combined_records.append(record) # Process Adjustments - if show_adjustments: - for adj in adjustments: - # Determine signed amount for display/total - amt = adj.amount - if adj.type in ['DEDUCTION', 'LOAN_REPAYMENT']: - amt = -amt + for adj in adjustments: + # Determine signed amount for display/total + amt = adj.amount + if adj.type in ['DEDUCTION', 'LOAN_REPAYMENT']: + amt = -amt + + record = { + 'type': 'ADJ', + 'date': adj.date, + 'obj': adj, + 'project_name': f"{adj.get_type_display()}", # Use project column for Type + 'team_name': None, + 'workers': [adj.worker], + 'supervisor': "System", + 'is_paid': adj.payroll_record is not None, + 'paid_record': adj.payroll_record, + 'notes': adj.description, + 'amount': amt if user_is_admin else None, + 'sort_id': adj.id + } + + if user_is_admin: + total_amount += amt - record = { - 'type': 'ADJ', - 'date': adj.date, - 'obj': adj, - 'project_name': f"{adj.get_type_display()}", # Use project column for Type - 'team_name': None, - 'workers': [adj.worker], - 'supervisor': "System", - 'is_paid': adj.payroll_record is not None, - 'paid_record': adj.payroll_record, - 'notes': adj.description, - 'amount': amt if user_is_admin else None, - 'sort_id': adj.id - } - - if user_is_admin: - total_amount += amt - - combined_records.append(record) + combined_records.append(record) # Sort combined list by Date Descending, then ID Descending combined_records.sort(key=lambda x: (x['date'], x['sort_id']), reverse=True) @@ -522,20 +534,23 @@ def export_work_log_csv(request): logs = logs.filter(paid_in__isnull=True) # --- 2. Fetch Adjustments --- - show_adjustments = True - if (project_id or team_id) and not worker_id: - show_adjustments = False + adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log') - adjustments = [] - if show_adjustments: - qs = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record') - if worker_id: - qs = qs.filter(worker_id=worker_id) - if payment_status == 'paid': - qs = qs.filter(payroll_record__isnull=False) - elif payment_status == 'unpaid': - qs = qs.filter(payroll_record__isnull=True) - adjustments = list(qs) + if worker_id: + adjustments = adjustments.filter(worker_id=worker_id) + + if project_id: + adjustments = adjustments.filter(work_log__project_id=project_id) + + if team_id: + adjustments = adjustments.filter(work_log__team_id=team_id) + + if payment_status == 'paid': + adjustments = adjustments.filter(payroll_record__isnull=False) + elif payment_status == 'unpaid': + adjustments = adjustments.filter(payroll_record__isnull=True) + + adjustments = list(adjustments) user_is_admin = is_admin(request.user) @@ -982,7 +997,8 @@ def price_overtime(request): type='OVERTIME', amount=amount, date=worklog.date, - description=f"Overtime: {worklog.get_overtime_display()} @ {rate_pct}% - {worklog.date.strftime('%d %b %Y')}" + description=f"Overtime: {worklog.get_overtime_display()} @ {rate_pct}% - {worklog.date.strftime('%d %b %Y')}", + work_log=worklog ) created += 1