Ver 16 What a mess but app is working

This commit is contained in:
Flatlogic Bot 2026-02-22 09:10:59 +00:00
parent 4ef75c9e8d
commit 6869282de2
10 changed files with 117 additions and 51 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.7 on 2026-02-22 08:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0017_alter_payrolladjustment_date_and_more'),
]
operations = [
migrations.AddField(
model_name='payrolladjustment',
name='project',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments_by_project', to='core.project'),
),
migrations.AlterField(
model_name='payrolladjustment',
name='work_log',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='adjustments_by_work_log', to='core.worklog'),
),
]

View File

@ -168,8 +168,8 @@ 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')
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_by_work_log')
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, related_name='adjustments' 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, 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)")
date = models.DateField(default=timezone.now, db_index=True) date = models.DateField(default=timezone.now, db_index=True)
@ -232,4 +232,4 @@ class ExpenseLineItem(models.Model):
verbose_name_plural = "Expense Line Items" verbose_name_plural = "Expense Line Items"
def __str__(self): def __str__(self):
return f"{self.product} - {self.amount}" return f"{self.product} - {self.amount}"

View File

@ -177,7 +177,7 @@
data-adj-description="{{ adj.description }}" data-adj-description="{{ adj.description }}"
data-adj-date="{{ adj.date|date:'Y-m-d' }}" data-adj-date="{{ adj.date|date:'Y-m-d' }}"
data-adj-worker="{{ item.worker.name }}" data-adj-worker="{{ item.worker.name }}"
data-adj-project="{% if adj.work_log %}{{ adj.work_log.project_id }}{% endif %}" data-adj-project="{% if adj.project_id %}{{ adj.project_id }}{% elif adj.work_log %}{{ adj.work_log.project_id }}{% endif %}"
title="{% if adj.type == 'ADVANCE' %}Click to delete (cannot edit){% else %}Click to edit{% endif %}"> title="{% if adj.type == 'ADVANCE' %}Click to delete (cannot edit){% else %}Click to edit{% endif %}">
{{ adj.get_type_display }}: R {{ adj.amount }} {% if adj.type != 'ADVANCE' %}✎{% endif %} {{ adj.get_type_display }}: R {{ adj.amount }} {% if adj.type != 'ADVANCE' %}✎{% endif %}
</span> </span>

View File

@ -140,15 +140,16 @@ def home(request):
if user_is_admin: if user_is_admin:
# Bulk-fetch all pending project-linked adjustments in one query # Bulk-fetch all pending project-linked adjustments in one query
pending_proj_adjs = {} pending_proj_adjs = {}
# Changed filter to look at adj.project or adj.work_log.project
for adj in PayrollAdjustment.objects.filter( for adj in PayrollAdjustment.objects.filter(
work_log__project__isnull=False, Q(project__isnull=False) | Q(work_log__project__isnull=False),
payroll_record__isnull=True payroll_record__isnull=True
).select_related('work_log'): ).select_related('work_log', 'project'):
pid = adj.work_log.project_id pid = adj.project_id or adj.work_log.project_id
pending_proj_adjs.setdefault(pid, []).append(adj) pending_proj_adjs.setdefault(pid, []).append(adj)
for project in all_projects: for project in all_projects:
outstanding_cost = 0 outstanding_cost = Decimal('0.00')
# Unpaid WorkLogs (use prefetch cache, check paid_in in Python) # Unpaid WorkLogs (use prefetch cache, check paid_in in Python)
for log in project.logs.all(): for log in project.logs.all():
@ -394,14 +395,14 @@ def work_log_list(request):
logs = logs.filter(paid_in__isnull=True) logs = logs.filter(paid_in__isnull=True)
# --- 2. Fetch Adjustments --- # --- 2. Fetch Adjustments ---
adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log', 'work_log__project') adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log', 'work_log__project', 'project')
if worker_id: if worker_id:
adjustments = adjustments.filter(worker_id=worker_id) adjustments = adjustments.filter(worker_id=worker_id)
if project_id: if project_id:
# Include only adjustments linked to this project (via work_log) # Include only adjustments linked to this project (via work_log OR direct project field)
adjustments = adjustments.filter(work_log__project_id=project_id) adjustments = adjustments.filter(Q(work_log__project_id=project_id) | Q(project_id=project_id))
if team_id: if team_id:
adjustments = adjustments.filter(work_log__team_id=team_id) adjustments = adjustments.filter(work_log__team_id=team_id)
@ -439,7 +440,7 @@ def work_log_list(request):
# --- 4. Combine and Sort --- # --- 4. Combine and Sort ---
user_is_admin = is_admin(request.user) user_is_admin = is_admin(request.user)
total_amount = 0 total_amount = Decimal('0.00')
combined_records = [] combined_records = []
# Prepare Chart Data (Overtime) - Admin only # Prepare Chart Data (Overtime) - Admin only
@ -493,11 +494,18 @@ def work_log_list(request):
if adj.type in DEDUCTIVE_TYPES: if adj.type in DEDUCTIVE_TYPES:
amt = -amt amt = -amt
# Determine Project Name
pname = adj.get_type_display()
if adj.project:
pname = f"{adj.get_type_display()} ({adj.project.name})"
elif adj.work_log and adj.work_log.project:
pname = f"{adj.get_type_display()} ({adj.work_log.project.name})"
record = { record = {
'type': 'ADJ', 'type': 'ADJ',
'date': adj.date, 'date': adj.date,
'obj': adj, 'obj': adj,
'project_name': f"{adj.get_type_display()} ({adj.work_log.project.name})" if adj.work_log and getattr(adj.work_log, 'project', None) else f"{adj.get_type_display()}", # Use project column for Type 'project_name': pname, # Use project column for Type
'team_name': None, 'team_name': None,
'workers': [adj.worker], 'workers': [adj.worker],
'supervisor': "System", 'supervisor': "System",
@ -637,13 +645,13 @@ def export_work_log_csv(request):
logs = logs.filter(paid_in__isnull=True) logs = logs.filter(paid_in__isnull=True)
# --- 2. Fetch Adjustments --- # --- 2. Fetch Adjustments ---
adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log', 'work_log__project') adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record', 'work_log', 'work_log__project', 'project')
if worker_id: if worker_id:
adjustments = adjustments.filter(worker_id=worker_id) adjustments = adjustments.filter(worker_id=worker_id)
if project_id: if project_id:
adjustments = adjustments.filter(work_log__project_id=project_id) adjustments = adjustments.filter(Q(work_log__project_id=project_id) | Q(project_id=project_id))
if team_id: if team_id:
adjustments = adjustments.filter(work_log__team_id=team_id) adjustments = adjustments.filter(work_log__team_id=team_id)
@ -675,7 +683,7 @@ def export_work_log_csv(request):
else: else:
workers_str = ", ".join([w.name for w in log.workers.all()]) workers_str = ", ".join([w.name for w in log.workers.all()])
amt = 0 amt = Decimal('0.00')
is_paid = False is_paid = False
if user_is_admin: if user_is_admin:
if target_worker: if target_worker:
@ -701,9 +709,15 @@ def export_work_log_csv(request):
is_paid = adj.payroll_record is not None is_paid = adj.payroll_record is not None
pname = adj.get_type_display()
if adj.project:
pname = f"{adj.get_type_display()} ({adj.project.name})"
elif adj.work_log and adj.work_log.project:
pname = f"{adj.get_type_display()} ({adj.work_log.project.name})"
combined.append({ combined.append({
'date': adj.date, 'date': adj.date,
'desc': f"{adj.get_type_display()} - {adj.description}", 'desc': f"{pname} - {adj.description}",
'workers': adj.worker.name, 'workers': adj.worker.name,
'amount': amt, 'amount': amt,
'status': "Paid" if is_paid else "Pending", 'status': "Paid" if is_paid else "Pending",
@ -774,7 +788,7 @@ def payroll_dashboard(request):
status_filter = request.GET.get('status', 'pending') # pending, paid, all, loans status_filter = request.GET.get('status', 'pending') # pending, paid, all, loans
# Common Analytics (prefetch all related data to avoid per-worker queries) # Common Analytics (prefetch all related data to avoid per-worker queries)
outstanding_total = 0 outstanding_total = Decimal('0.00')
active_workers = Worker.objects.filter(is_active=True).order_by('name').prefetch_related( active_workers = Worker.objects.filter(is_active=True).order_by('name').prefetch_related(
Prefetch( Prefetch(
'work_logs', 'work_logs',
@ -785,7 +799,7 @@ def payroll_dashboard(request):
), ),
Prefetch( Prefetch(
'adjustments', 'adjustments',
queryset=PayrollAdjustment.objects.filter(payroll_record__isnull=True), queryset=PayrollAdjustment.objects.filter(payroll_record__isnull=True).select_related('project', 'work_log', 'work_log__project'),
to_attr='pending_adjustments_list' to_attr='pending_adjustments_list'
), ),
) )
@ -867,27 +881,34 @@ def payroll_dashboard(request):
Prefetch('logs', queryset=WorkLog.objects.prefetch_related('workers', 'paid_in')), Prefetch('logs', queryset=WorkLog.objects.prefetch_related('workers', 'paid_in')),
) )
# Bulk-fetch all pending project-linked adjustments in one query # Bulk-fetch all project-linked adjustments
pending_proj_adjs = {} all_proj_adjs = {}
for adj in PayrollAdjustment.objects.filter( for adj in PayrollAdjustment.objects.filter(
work_log__project__isnull=False, Q(project__isnull=False) | Q(work_log__project__isnull=False)
work_log__project__is_active=True, ).select_related('work_log', 'project'):
payroll_record__isnull=True, pid = adj.project_id or adj.work_log.project_id
).select_related('work_log'): all_proj_adjs.setdefault(pid, []).append(adj)
pid = adj.work_log.project_id
pending_proj_adjs.setdefault(pid, []).append(adj)
for project in active_projects: for project in active_projects:
# 1. Total Historical Cost # 1. Total Historical Cost
cost = 0 cost = Decimal('0.00')
# WorkLogs
for log in project.logs.all(): for log in project.logs.all():
for worker in log.workers.all(): for worker in log.workers.all():
cost += worker.day_rate cost += worker.day_rate
# Adjustments
for adj in all_proj_adjs.get(project.id, []):
if adj.type in ADDITIVE_TYPES:
cost += adj.amount
elif adj.type in DEDUCTIVE_TYPES:
cost -= adj.amount
if cost > 0: if cost > 0:
project_costs.append({'name': project.name, 'cost': cost}) project_costs.append({'name': project.name, 'cost': cost})
# 2. Outstanding Cost (Unpaid) # 2. Outstanding Cost (Unpaid)
outstanding_cost = 0 outstanding_cost = Decimal('0.00')
# Unpaid WorkLogs (check paid_in in Python using prefetch cache) # Unpaid WorkLogs (check paid_in in Python using prefetch cache)
for log in project.logs.all(): for log in project.logs.all():
@ -895,12 +916,13 @@ def payroll_dashboard(request):
for worker in log.workers.all(): for worker in log.workers.all():
outstanding_cost += worker.day_rate outstanding_cost += worker.day_rate
# Unpaid Adjustments linked to this project (from bulk-fetched dict) # Unpaid Adjustments linked to this project
for adj in pending_proj_adjs.get(project.id, []): for adj in all_proj_adjs.get(project.id, []):
if adj.type in ADDITIVE_TYPES: if adj.payroll_record_id is None:
outstanding_cost += adj.amount if adj.type in ADDITIVE_TYPES:
elif adj.type in DEDUCTIVE_TYPES: outstanding_cost += adj.amount
outstanding_cost -= adj.amount elif adj.type in DEDUCTIVE_TYPES:
outstanding_cost -= adj.amount
if outstanding_cost > 0: if outstanding_cost > 0:
outstanding_project_costs.append({'name': project.name, 'cost': outstanding_cost}) outstanding_project_costs.append({'name': project.name, 'cost': outstanding_cost})
@ -960,10 +982,11 @@ def payroll_dashboard(request):
ot_history_totals = [ot_by_month.get(ym, 0) for ym in chart_months] ot_history_totals = [ot_by_month.get(ym, 0) for ym in chart_months]
# 1 query + prefetch: all work logs in the 6-month period for per-project costs # Per-project costs (last 6 months)
all_project_names = list(Project.objects.values_list('name', flat=True).order_by('name')) all_project_names = list(Project.objects.values_list('name', flat=True).order_by('name'))
project_monthly = {name: [0] * len(chart_months) for name in all_project_names} project_monthly = {name: [0] * len(chart_months) for name in all_project_names}
# 1. Work Logs
all_chart_logs = WorkLog.objects.filter( all_chart_logs = WorkLog.objects.filter(
date__gte=chart_start date__gte=chart_start
).select_related('project').prefetch_related('workers') ).select_related('project').prefetch_related('workers')
@ -977,6 +1000,26 @@ def payroll_dashboard(request):
for worker in log.workers.all(): for worker in log.workers.all():
project_monthly[pname][idx] += float(worker.day_rate) project_monthly[pname][idx] += float(worker.day_rate)
# 2. Adjustments
all_chart_adjs = PayrollAdjustment.objects.filter(
date__gte=chart_start,
payroll_record__isnull=False # Only count paid adjustments for historical chart? Or all?
# Actually WorkLog chart includes all logs. Let's include all adjustments for consistency.
).select_related('project', 'work_log', 'work_log__project')
for adj in all_chart_adjs:
month_key = (adj.date.year, adj.date.month)
idx = chart_month_index.get(month_key)
if idx is not None:
project = adj.project or (adj.work_log.project if adj.work_log else None)
if project:
pname = project.name
if pname in project_monthly:
amt = float(adj.amount)
if adj.type in DEDUCTIVE_TYPES:
amt = -amt
project_monthly[pname][idx] += amt
# Filter out projects with zero cost across all months # Filter out projects with zero cost across all months
project_chart_data = [ project_chart_data = [
{'name': name, 'data': costs} {'name': name, 'data': costs}
@ -1185,7 +1228,8 @@ def price_overtime(request):
amount=amount, amount=amount,
date=worklog.date, 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 work_log=worklog,
project=worklog.project # Link project directly
) )
created += 1 created += 1
@ -1296,6 +1340,10 @@ def add_adjustment(request):
messages.error(request, "Amount must be greater than zero.") messages.error(request, "Amount must be greater than zero.")
return redirect('payroll_dashboard') return redirect('payroll_dashboard')
project = None
if project_id:
project = get_object_or_404(Project, pk=project_id)
if worker_ids and amount and adj_type: if worker_ids and amount and adj_type:
success_names = [] success_names = []
skip_names = [] skip_names = []
@ -1335,10 +1383,6 @@ def add_adjustment(request):
if project_id: if project_id:
# Try to find a worklog for this worker and project to link the adjustment to the project # Try to find a worklog for this worker and project to link the adjustment to the project
work_log_link = worker.work_logs.filter(project_id=project_id).order_by('-date').first() work_log_link = worker.work_logs.filter(project_id=project_id).order_by('-date').first()
if not work_log_link:
# Fallback: any worklog for project
from .models import WorkLog
work_log_link = WorkLog.objects.filter(project_id=project_id).order_by('-date').first()
PayrollAdjustment.objects.create( PayrollAdjustment.objects.create(
worker=worker, worker=worker,
@ -1347,6 +1391,7 @@ def add_adjustment(request):
description=description or 'Advance payment', description=description or 'Advance payment',
date=date, date=date,
work_log=work_log_link, work_log=work_log_link,
project=project
) )
advance_date = date if isinstance(date, datetime.date) else timezone.now().date() advance_date = date if isinstance(date, datetime.date) else timezone.now().date()
@ -1354,7 +1399,6 @@ def add_adjustment(request):
worker=worker, worker=worker,
amount=advance_amount, amount=advance_amount,
date=advance_date, date=advance_date,
type='ADVANCE',
notes=description or 'Advance payment' notes=description or 'Advance payment'
) )
@ -1416,10 +1460,6 @@ def add_adjustment(request):
if project_id: if project_id:
# Try to find a worklog for this worker and project to link the adjustment to the project # Try to find a worklog for this worker and project to link the adjustment to the project
work_log_link = worker.work_logs.filter(project_id=project_id).order_by('-date').first() work_log_link = worker.work_logs.filter(project_id=project_id).order_by('-date').first()
if not work_log_link:
# Fallback: any worklog for project
from .models import WorkLog
work_log_link = WorkLog.objects.filter(project_id=project_id).order_by("-date").first()
PayrollAdjustment.objects.create( PayrollAdjustment.objects.create(
worker=worker, worker=worker,
@ -1428,7 +1468,8 @@ def add_adjustment(request):
description=description, description=description,
date=date, date=date,
loan=loan, loan=loan,
work_log=work_log_link work_log=work_log_link,
project=project
) )
success_names.append(worker.name) success_names.append(worker.name)
@ -1479,12 +1520,13 @@ def edit_adjustment(request, pk):
adj.date = date adj.date = date
if project_id: if project_id:
project = get_object_or_404(Project, pk=project_id)
adj.project = project
# Try to update work_log link too
work_log_link = adj.worker.work_logs.filter(project_id=project_id).order_by('-date').first() work_log_link = adj.worker.work_logs.filter(project_id=project_id).order_by('-date').first()
if not work_log_link:
from .models import WorkLog
work_log_link = WorkLog.objects.filter(project_id=project_id).order_by('-date').first()
adj.work_log = work_log_link adj.work_log = work_log_link
elif project_id == '': elif project_id == '':
adj.project = None
adj.work_log = None adj.work_log = None
# Only allow type change for BONUS/DEDUCTION (others have linked objects) # Only allow type change for BONUS/DEDUCTION (others have linked objects)
@ -1630,4 +1672,4 @@ def create_receipt(request):
return render(request, 'core/create_receipt.html', { return render(request, 'core/create_receipt.html', {
'form': form, 'form': form,
'items': items 'items': items
}) })