diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index b372d45..ff36ed9 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/work_log_list.html b/core/templates/core/work_log_list.html index 187e549..78cb1d9 100644 --- a/core/templates/core/work_log_list.html +++ b/core/templates/core/work_log_list.html @@ -9,7 +9,7 @@

Work Log History

-

Filter and review historical daily work logs.

+

Filter and review historical daily work logs and adjustments.

@@ -142,31 +142,31 @@ {% for week in calendar_weeks %} {% for day_info in week %} -
{{ day_info.day }} - {% if day_info.logs %} - {{ day_info.logs|length }} + {% if day_info.records %} + {{ day_info.records|length }} {% endif %}
- {% for log in day_info.logs %} -
-
{{ log.project.name }}
- {% if log.team %} + {% for record in day_info.records %} +
+
{{ record.project_name }}
+ {% if record.type == 'WORK' and record.team_name %}
- {{ log.team.name }} + {{ record.team_name }}
{% endif %} -
+
{% if selected_worker %} - {{ log.workers.first.name }} - {% elif log.workers.count == 1 %} - {{ log.workers.first.name }} + {{ record.workers.0.name }} + {% elif record.workers|length == 1 %} + {{ record.workers.0.name }} {% else %} - {{ log.workers.count }} workers + {{ record.workers|length }} workers {% endif %}
@@ -196,14 +196,14 @@
- {% if logs %} + {% if records %}
- - + + {% if is_admin_user %}{% endif %} {% if is_admin_user %}{% endif %} @@ -211,61 +211,70 @@ - {% for log in logs %} + {% for record in records %} {% if is_admin_user %} {% endif %}
DateProjectLabourersDescription / ProjectWorker(s)AmountStatus / PayslipSupervisor
- {{ log.date|date:"D, d M Y" }} + {{ record.date|date:"D, d M Y" }} - - {{ log.project.name }} - + {% if record.type == 'WORK' %} + + {{ record.project_name }} + + {% if record.team_name %} + {{ record.team_name }} + {% endif %} + {% else %} + + {{ record.project_name }} + + {% endif %} {% if selected_worker %} - {% for w in log.workers.all %} + {% for w in record.workers %} {% if w.id == selected_worker %} {{ w.name }} {% endif %} {% endfor %} - {% if log.workers.count > 1 %} - (+{{ log.workers.count|add:"-1" }} others) + {% if record.workers|length > 1 %} + (+{{ record.workers|length|add:"-1" }} others) {% endif %} {% else %}
- {% for w in log.workers.all|slice:":3" %} + {% for w in record.workers|slice:":3" %} {{ w.name|truncatechars:12 }} {% endfor %} - {% if log.workers.count > 3 %} - +{{ log.workers.count|add:"-3" }} + {% if record.workers|length > 3 %} + +{{ record.workers|length|add:"-3" }} {% endif %}
{% endif %}
- R {{ log.display_amount|floatformat:2 }} + + R {{ record.amount|floatformat:2 }} + - {% with payslip=log.paid_in.first %} - {% if payslip %} - - - Paid (Slip #{{ payslip.id }}) - - - {% else %} - - Pending + {% if record.paid_record %} + + + Paid (Slip #{{ record.paid_record.id }}) - {% endif %} - {% endwith %} + + {% else %} + + Pending + + {% endif %} - {{ log.supervisor.username|default:"System" }} + {{ record.supervisor }} @@ -298,7 +312,7 @@ {% else %}
-

No logs found matching filters.

+

No records found matching filters.

Try adjusting your filters or record a new entry.

Log Attendance
@@ -362,30 +376,41 @@ document.addEventListener('DOMContentLoaded', function() { let html = '
'; html += ''; if (sorted.length > 1) html += ''; - html += ''; + html += ''; sorted.forEach(function(dateStr) { - const logs = detailData[dateStr] || []; + const records = detailData[dateStr] || []; const d = new Date(dateStr + 'T00:00:00'); const dateLabel = d.toLocaleDateString('en-ZA', dateOpts); - logs.forEach(function(log, idx) { - const teams = log.teams.length ? log.teams.join(', ') : '-'; - const workers = log.workers.map(function(w) { + records.forEach(function(rec, idx) { + const teams = rec.teams.length ? rec.teams.join(', ') : ''; + + let desc = rec.project; + if(rec.type === 'ADJ') { + desc = '' + rec.project + ''; + } else { + desc = '' + rec.project + ''; + if (teams) desc += ' ' + teams + ''; + } + + const workers = rec.workers.map(function(w) { return '' + w + ''; }).join(' '); - const notes = log.notes ? '' + log.notes + '' : '-'; + + const notes = rec.notes ? '' + rec.notes + '' : '-'; + const amount = rec.amount ? 'R ' + rec.amount.toFixed(2) : '-'; html += ''; if (sorted.length > 1) { if (idx === 0) { - html += ''; + html += ''; } } - html += ''; - html += ''; + html += ''; html += ''; - html += ''; + html += ''; + html += ''; html += ''; html += ''; }); @@ -400,8 +425,8 @@ document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.cal-day--has-logs').forEach(function(cell) { cell.addEventListener('click', function(e) { const dateStr = this.dataset.date; - const logs = detailData[dateStr]; - if (!logs || !logs.length) return; + const records = detailData[dateStr]; + if (!records || !records.length) return; if (e.shiftKey) { // Shift+click: toggle this date in multi-selection diff --git a/core/views.py b/core/views.py index 04c4ab4..6f9408b 100644 --- a/core/views.py +++ b/core/views.py @@ -262,7 +262,7 @@ def log_attendance(request): @login_required def work_log_list(request): - """View work log history with advanced filtering.""" + """View work log history and payroll adjustments with advanced filtering.""" if not is_staff_or_supervisor(request.user): return redirect('log_attendance') @@ -272,12 +272,12 @@ def work_log_list(request): payment_status = request.GET.get('payment_status') # 'paid', 'unpaid', 'all' view_mode = request.GET.get('view', 'list') + # --- 1. Fetch WorkLogs --- logs = WorkLog.objects.all().select_related('team', 'project', 'supervisor').prefetch_related('workers', 'paid_in').order_by('-date', '-id') target_worker = None if worker_id: logs = logs.filter(workers__id=worker_id) - # Fetch the worker to get the day rate reliably target_worker = Worker.objects.filter(id=worker_id).first() if team_id: @@ -287,24 +287,38 @@ def work_log_list(request): logs = logs.filter(project_id=project_id) if payment_status == 'paid': - # Logs that are linked to at least one PayrollRecord logs = logs.filter(paid_in__isnull=False).distinct() elif payment_status == 'unpaid': - # This is tricky because a log can have multiple workers, some paid some not. - # But usually a WorkLog is marked paid when its workers are paid. - # If we filtered by worker, we can check if THAT worker is paid in that log. if worker_id: worker = get_object_or_404(Worker, pk=worker_id) logs = logs.exclude(paid_in__worker=worker) else: logs = logs.filter(paid_in__isnull=True) - # Calculate amounts for display - # Convert to list to attach attributes - final_logs = [] - total_amount = 0 + # --- 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.none() + if show_adjustments: + adjustments = PayrollAdjustment.objects.all().select_related('worker', 'payroll_record') + if worker_id: + adjustments = adjustments.filter(worker_id=worker_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 + end_date = None + curr_year = timezone.now().year + curr_month = timezone.now().month - # If Calendar View: Filter logs by Month BEFORE iterating to prevent fetching ALL history if view_mode == 'calendar': today = timezone.now().date() try: @@ -314,29 +328,81 @@ def work_log_list(request): curr_year = today.year curr_month = today.month - # Bounds safety if curr_month < 1: curr_month = 1; if curr_month > 12: curr_month = 12; - # Get range _, num_days = calendar.monthrange(curr_year, curr_month) start_date = datetime.date(curr_year, curr_month, 1) 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)) + # --- 4. Combine and Sort --- user_is_admin = is_admin(request.user) + total_amount = 0 + combined_records = [] + # Process Logs for log in logs: + record = { + 'type': 'WORK', + 'date': log.date, + 'obj': log, + 'project_name': log.project.name, + 'team_name': log.team.name if log.team else None, + 'workers': list(log.workers.all()), + 'supervisor': log.supervisor.username if log.supervisor else "System", + 'is_paid': log.paid_in.exists() if not worker_id else log.paid_in.filter(worker_id=worker_id).exists(), + 'paid_record': log.paid_in.first() if not worker_id else log.paid_in.filter(worker_id=worker_id).first(), + 'notes': log.notes, + 'sort_id': log.id + } + + # Calculate amount if user_is_admin: if target_worker: - log.display_amount = target_worker.day_rate + amt = target_worker.day_rate else: - log.display_amount = sum(w.day_rate for w in log.workers.all()) - total_amount += log.display_amount + amt = sum(w.day_rate for w in log.workers.all()) + record['amount'] = amt + total_amount += amt else: - log.display_amount = None - final_logs.append(log) + record['amount'] = None + + 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 + + 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) + + # Sort combined list by Date Descending, then ID Descending + combined_records.sort(key=lambda x: (x['date'], x['sort_id']), reverse=True) # Context for filters context = { @@ -355,11 +421,12 @@ def work_log_list(request): if view_mode == 'calendar': # Group by date for easy lookup in template - logs_map = {} - for log in final_logs: - if log.date not in logs_map: - logs_map[log.date] = [] - logs_map[log.date].append(log) + records_map = {} + for rec in combined_records: + d = rec['date'] + if d not in records_map: + records_map[d] = [] + records_map[d].append(rec) cal = calendar.Calendar(firstweekday=0) # Monday is 0 month_dates = cal.monthdatescalendar(curr_year, curr_month) @@ -373,24 +440,28 @@ def work_log_list(request): 'date': d, 'day': d.day, 'is_current_month': d.month == curr_month, - 'logs': logs_map.get(d, []) + 'records': records_map.get(d, []) }) calendar_weeks.append(week_data) # Build JSON lookup for day detail panel calendar_detail_data = {} - for date_key, day_logs in logs_map.items(): + for date_key, day_records in records_map.items(): date_str = date_key.strftime('%Y-%m-%d') calendar_detail_data[date_str] = [] - for log in day_logs: - workers_list = list(log.workers.all().order_by('name')) - team_name = log.team.name if log.team else 'No Team' + for rec in day_records: + workers_list = [w.name for w in rec['workers']] + team_name = rec['team_name'] if rec['team_name'] else '' + + # Format for JS calendar_detail_data[date_str].append({ - 'project': log.project.name, - 'teams': [team_name], - 'workers': [w.name for w in workers_list], - 'supervisor': log.supervisor.username if log.supervisor else 'System', - 'notes': log.notes or '', + 'project': rec['project_name'], # This holds Type for ADJ + 'teams': [team_name] if team_name else [], + 'workers': workers_list, + 'supervisor': rec['supervisor'], + 'notes': rec['notes'] or '', + 'type': rec['type'], + 'amount': float(rec['amount']) if rec['amount'] is not None else 0 }) # Nav Links @@ -409,13 +480,13 @@ def work_log_list(request): 'next_year': next_month_date.year, }) else: - context['logs'] = final_logs + context['records'] = combined_records return render(request, 'core/work_log_list.html', context) @login_required def export_work_log_csv(request): - """Export filtered work logs to CSV.""" + """Export filtered work logs and adjustments to CSV.""" if not is_staff_or_supervisor(request.user): return HttpResponse("Unauthorized", status=401) @@ -424,6 +495,7 @@ def export_work_log_csv(request): project_id = request.GET.get('project') payment_status = request.GET.get('payment_status') + # --- 1. Fetch WorkLogs --- logs = WorkLog.objects.all().prefetch_related('workers', 'workers__teams', 'project', 'supervisor', 'paid_in').order_by('-date', '-id') target_worker = None @@ -447,39 +519,89 @@ def export_work_log_csv(request): else: 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 = [] + 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) + user_is_admin = is_admin(request.user) response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="work_logs.csv"' + response['Content-Disposition'] = 'attachment; filename="work_logs_and_adjustments.csv"' writer = csv.writer(response) if user_is_admin: - writer.writerow(['Date', 'Project', 'Workers', 'Amount', 'Payment Status', 'Supervisor']) + writer.writerow(['Date', 'Description', 'Workers', 'Amount', 'Payment Status', 'Supervisor']) else: - writer.writerow(['Date', 'Project', 'Workers', 'Supervisor']) + writer.writerow(['Date', 'Description', 'Workers', 'Supervisor']) + # Combine and Sort + combined = [] + for log in logs: if target_worker: workers_str = target_worker.name else: workers_str = ", ".join([w.name for w in log.workers.all()]) - + + amt = 0 + is_paid = False if user_is_admin: if target_worker: - display_amount = target_worker.day_rate + amt = target_worker.day_rate + is_paid = log.paid_in.filter(worker=target_worker).exists() else: - display_amount = sum(w.day_rate for w in log.workers.all()) - is_paid = log.paid_in.exists() - status_str = "Paid" if is_paid else "Pending" + amt = sum(w.day_rate for w in log.workers.all()) + is_paid = log.paid_in.exists() + + combined.append({ + 'date': log.date, + 'desc': log.project.name, + 'workers': workers_str, + 'amount': amt, + 'status': "Paid" if is_paid else "Pending", + 'supervisor': log.supervisor.username if log.supervisor else "System" + }) + + for adj in adjustments: + amt = adj.amount + if adj.type in ['DEDUCTION', 'LOAN_REPAYMENT']: + amt = -amt + + is_paid = adj.payroll_record is not None + + combined.append({ + 'date': adj.date, + 'desc': f"{adj.get_type_display()} - {adj.description}", + 'workers': adj.worker.name, + 'amount': amt, + 'status': "Paid" if is_paid else "Pending", + 'supervisor': "System" + }) + + # Sort + combined.sort(key=lambda x: x['date'], reverse=True) + + for row in combined: + if user_is_admin: writer.writerow([ - log.date, log.project.name, workers_str, - f"{display_amount:.2f}", status_str, - log.supervisor.username if log.supervisor else "System" + row['date'], row['desc'], row['workers'], + f"{row['amount']:.2f}", row['status'], row['supervisor'] ]) else: writer.writerow([ - log.date, log.project.name, workers_str, - log.supervisor.username if log.supervisor else "System" + row['date'], row['desc'], row['workers'], row['supervisor'] ]) return response
DateProjectTeam(s)WorkersSupervisorNotes
Description / ProjectWorkersAmountSupervisorNotes
' + dateLabel + '' + dateLabel + '' + log.project + '' + teams + '' + desc + '' + workers + '' + log.supervisor + '' + amount + '' + rec.supervisor + '' + notes + '