From df4cadf0249170044096f96844ac79fae5f29a44 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 8 Feb 2026 23:26:12 +0000 Subject: [PATCH] Ver 14.03 Adjustments added to work log --- core/__pycache__/views.cpython-311.pyc | Bin 47869 -> 51995 bytes core/templates/core/work_log_list.html | 141 +++++++++------- core/views.py | 218 +++++++++++++++++++------ 3 files changed, 253 insertions(+), 106 deletions(-) diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index b372d45f9af17a20f844e9cb40ce029d6d55bdfb..ff36ed9f9d5ade452c7cbb52d677e828f84f3e3c 100644 GIT binary patch delta 9281 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 UEDKj9#AUWbBJx?`>ucHn0e5sy!2kdN delta 5758 zcma)A4Rll2k$zAAvVLtzw*04m*_It_|uEgSG-G@A|kFhs)4a(mjoFwwnR0N#hG;s)03Z}c{Q(j9Zqmj#B|T_z74~u zHmZx%W`tmYAw%N=uK0Cu$whRF@QdpIyezMKKsK9>E-@GIdOo{LbjkWU3yR=xMv*MC zl*VdYE2BBQYz^~FC~DwyNINel8eZO0lo8_%-r@`$tc@(s&853h7T;T%^* zjq`#~M4SO8YT}jPyISy_Vcxx&cC#d_o8e9KxsQSC7IN|#8sB4S;Jw2&`Id)-;c7xuU_+JT;p zcV3fz*LV~4J)c)m*v-sD?u;Vbh-okzO3N z@Wm6Q^wliW!zBxsN3GzmBHk)^$pR}TqK3%EjA`JxOr&k!rlZ4JlZbOrcE6<>_;6wA zA5qUA^^|A$aJ8jTJ8DmRya>c9GLv&+yuF*{%BX$6aR>w+$_NNrXGcAYGJMeL0lC>E z($8|`T*bI_;km#qXOI1qU-;HmWpr@GmmXRuKL|-NTI=Am7rf@*v;cHPoxE|Pn%-9$ zD6~eCHgWTQsg*sF`Jg zQeMd$xk|jrNFZYY+#thM@y3N2Y7^tB&Ks=U67cV_?Fo@xw%$;;ZD$}H36Xs?uknVI zV8*AqwNCo|wMS~&A$x^g!1+)3Z&Ix$hAM*uG>6Gc zz#5ddg>0MszCPNvVPDgWsImaqpw7BH{Y);NV9(2Dhk~M%Y}3AQ#NU@s4Dyu6H^!0iL58pxVa|qK2zr}ghn^FV^`n;TPUzog$?1KpX2>TEQ z0a6NF@9p#Tr^KDU-ju2@6pZYIH4%T9{00Rid;I=gV4DX1LB5ZC0BpLt)vi#0YTTcO zTJu#YWdv^5i^rxfw>nuDeXF&U-f1Y!>xxERcRKWAd!@`HdTg z?iq{5o*e&n+*qD4mJhey)fuK4rPM0Gh&ZV-Pcw35Nm5rd&4@J}Y|>qma6dXKPUsym zz2ly{Y$lR7U3>bv_kC}Cd0S$68}Q;~?Fm44d&1o=NGH|ivDJy9%DB36q-CV#uH8Md zI-$1YB^5ch6!|w5`D4zw!je!}V!}hWZ2m=)!8D>s8j4|OWrYAEtCPB%V|&N;#&o6d zB;BQ>iZR2uIj(mk)yC0HC$@shTWa@BwR@s7t}aig%VWYr_dF5Q7rB1Wa;9Z6d$J*3 zRFx>IqMtvpve;36q4Hd1+)CjbT)L+v=QH zeZKiZ`?>bGt2W`Pz2&OE>8iiFJMLPWaIKA7*Cnj$M%Bqh)t9!$iq?#+9$!D+I@Wr% zZd94nyC$|K^p&^twKw&(NvmUgf5KXow3LoLofO{RD{6Rg+c`+66Pa`xydU2$if2B`+u1$GRvWlt(aQ1 z^`FI}h#kAF%BV6Tg%vYDuJFW;iRI ze#Ru8VJriN?Hv1HBIc!QpjUn-A39kXQSh=JV`kf^Cb)#NPnfU}%N@|hsi7^NYvZho zG_c?4nN@kStF*!D`F#oJ{OU@n7Ifv)Pl_MPiRySAw8Ji_lkTWqFdo{JUFx2~j6x7C z%FuJhL1of%C9g@KZB@^;k5Gl@je0DZby(ZGWe*HWMK5+%>x&>A z2_sUBl$HLv^N1+U(lA#7C9hsiehHG~UuhL*Vs9V!S9ot>{+d?Tw3+01|@gxrvj@gotQVZERU17pP0TC)eN3_7&Yrx|n%;`iU!i3572vJa;p5Yhq@6 z;)=$EqA{ju6q0+cRCk{Npug>D7{c_fSL$ZcdkdSiS0?OBN42Bcdv;g)My?vJ6<)W# zZ;jVACF+`h7k4!$0PW2Qd-JF^nVWxN*R9;Lo4IB2-108UqL>7~A2 zSq=F8SL~D}SRt0C#GxHMwCy>MYz1&XW2d6eJ*8#MG&xk%oKpF?o`G z`>Hnj^txh+M?_Z9isAM02G|N36goIO$o>oceE2B4lMWqN#BQRO4{Tz$QTE`YEKipo z{2hCnEls#EABRRW!ED@gAU?AFW#hgroASUxB@ldNmb{n9{$E7r4(>|-d^}J zNuNtN;T_s{u3YvuDEgyl>U-zzibbGm==E3GU()Og9^-dmA}oO&Y&P34@!(dsY1@So zb}1dWuvL%K=}ua(^)kJE;qh+?q>yO^(}bp6kD7P2@Z;P=B#-xhVpuZnj^t~S&Aa?)Z z$IYIdxJ-y$p>*Mur$2?ZBXtD4(6yb&*h?jl2vzk0>RF z_Yb**p`Tu_H@ynnlwwBGON78bK_C0@1PlKQ=)+2DoiHB=LP-mD`UwU=O0nCQ4q0-C zT5c?6PtlGWX4e=F zH2WleXA!Kz1j_-z>`yS85ljIDUl@XsN-*pQ1{6V;3);!cs0csZg&_Sx@mODo8|d}d zlTX>HLqDq#)ol^6E2bI5`(j3>5?K8b7)^`Tu&i;KL7bfySS_Nd-~8Po_WuCP C@6|p4 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 + '