From fa07cb69a88e814d408deeebde1c9564f1c8db4f Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 3 Feb 2026 23:08:10 +0000 Subject: [PATCH] ver 7.7 --- core/__pycache__/urls.cpython-311.pyc | Bin 1155 -> 1268 bytes core/__pycache__/views.cpython-311.pyc | Bin 17145 -> 21302 bytes core/templates/core/work_log_list.html | 22 ++++++- core/urls.py | 4 +- core/views.py | 87 +++++++++++++++++++++++-- 5 files changed, 105 insertions(+), 8 deletions(-) diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 0cae82c8663ee8f280c658ad00164a3ad354c596..20b523f95c07178c40901876a29745a5c47e0ff7 100644 GIT binary patch delta 423 zcmZqX{KBceoR^o20SFdUH)YOYW?*;>;=lkel<`?-qPjUtE_W37#JEcqObn@vDWa*| zDPn8bmI1Y_24V5f5h2lz0ge&}6*DQjl1Z@sbh5;hgNps3szuT2YW+R1#mF zUz8o6lb;@+TwFG}m~ommNI(}TP^=G8pS6KWeGqV)( Q3v=-^HE@GqkqFSq02Pd65dZ)H delta 348 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 delta 3081 zcmZ`*Yit}>6`tAIr}yoB*j}&gUB|J#ahf7hx=Cryg#T}DAJ03CZ> zkc(;@uv<+4?oyM0J!&)HZZ!qitEK__)C^$1DxMZfSw19Q4~R}=!7^(bzZOsV#D3lp z_*NkSK5zo&`Ab^Gwj5u@T&dYC2~5_Ekm7#~EVg$c=|@eeW?pr?YT4FU7MW>29qh@J zklceXfbayuUj9mOpSX|T4tDwr$T-M<8*CGY_}_ys_~wC;*q9CdDkSE3H1&jk95qJy zWa`h}lE{wnLb_Kx!spV%;${Bj^j>j+-%jrl=lRF!@rG_|M#-PSY0m+ax?OvpM8*ih zBIlW&W1~Pi;VWf*MY9yWYT1q#ksU+6-_Y!frphv?e*!tds-=K(Rdpg2T`Si#Re`;h z9qEE*vjq2M=Lh$ra*C!i%`mTMj$hSu&DI>RRlcIJqbQOr%`PR`B!4B_du|G6kfpdw zCt-nl#fGJ?n(VU1EOrtVzJM@?3h`~eVw(yrIgQ+)sjG@^Ua;5<4N(cl?8SFla=9bO zi@{JS+qPCy%e9Ke>cE`d7-<>x<`;mhhyaZz1cU^?f5&`$YOH^w-ajTvb&>I8S6jL+ zt~cER?K?X&uKD=lp5bKpF|MC~P-+wXe6iyx-qu;jL?4s;>RvU*k9QWO__Icw47_j9M(=MwHfH1Y12#0@0=+w=i_B|*y6?R(FUHc|dSE^EPS-#>y`K1= zH}Yb~pjS<=XZQ~ayOY^R<#sDyD-1~gPnvsJnt5z4SR1!H_|alVpr!61db!wfvZXE| z-LP8_>e8d{tchw1_@VXN!D~SqOKJP53u;?EcnhL%2O~iblxmLurTFA3#++S6I1J## zmdh_2TFq80yKJvm4D;GewZL+@sw&kQYX#PQX|Sr6L?_%xA%%t^TPquu;tn2?)j+jr znVDGpAauOA)+{jN{APD&TOL+C!*cj4H2Zid$PVIQl)t-6;gO!)YQmA`X3nrGaxMTKcP^b3^@qXN|?hM8SL@F6^nKeS zFn~J&g1wG&j=zeCHaQUzY;T*9_pq2O%=1FyVN@p3M(I#PU^hVnf7T2i-~H7#IMYGs9LE)DI|Js3{hzlEY2Y)BFV0!~&$T65vZf?E7hen@Sy)W(s zk%`=P04HE*mIVvaGNlMxLJ67uz(fi;MGvPii{@R_$|^Rr7_#Wl@B&rxyJ2iHOz4z~ z{qnNL-bDS7%i#A%i_q-zhw}dK0{iy@pE&eh`?<}ob00{*h;ELYy?f;Dk{rvEBcX%(7`w)isOVj&S-$s&r@F1L&+7!vBP)b=%r^Nvr9zsY1I5GDC*r^M0 zDjG>@H0GRM`0Fnadss+xX0fQt=I34Reyh#<0gXvXiVD-x!#m7FTg*Dera(a`X~T`7HOM zXQ}%*{tDCZe&R>Y1Mk^5#58pilP&;0=$tjv4R#ns5df$8@XYM=b4Sl8v;5PA_zPGU zPH5&!6>ZtBnl)C#u>^MrM#&v^%ar4Es;(a*wc}sv+~g0YRZIpQ%vzz#7JHYUezvcj zI#L{ZFqEyOYzC7lu;F@tBqqT!#L=WHN*4Jh1p-6z)w} z$QHE({@2Cys^dekMHeY2h_^$#YHZ&S=h1*V0ybG{DGbY;vVt~BZ)IOZ9=?^*=zeev zUb3QV6YMv@6X9olA8<<&Me!3Mv__v#g!meK_^T(Q{Oc!gCVDo7N4|XO)Rp+4IJqV4 Oz>ECjQ)j*Sp8o=F-psTB diff --git a/core/templates/core/work_log_list.html b/core/templates/core/work_log_list.html index a6954d6..9c17790 100644 --- a/core/templates/core/work_log_list.html +++ b/core/templates/core/work_log_list.html @@ -11,9 +11,14 @@

Work Log History

Filter and review historical daily work logs.

- - + New Entry - + @@ -82,6 +87,7 @@ Date Project Labourers + Amount Status / Payslip Supervisor Action @@ -121,6 +127,9 @@ {% endif %} + + R {{ log.display_amount|floatformat:2 }} + {% with payslip=log.paid_in.first %} {% if payslip %} @@ -156,6 +165,13 @@ {% endfor %} + + + Total: + R {{ total_amount|floatformat:2 }} + + + {% else %} diff --git a/core/urls.py b/core/urls.py index b5f1475..f06bb24 100644 --- a/core/urls.py +++ b/core/urls.py @@ -2,7 +2,8 @@ from django.urls import path from .views import ( home, log_attendance, - work_log_list, + work_log_list, + export_work_log_csv, manage_resources, toggle_resource_status, payroll_dashboard, @@ -14,6 +15,7 @@ urlpatterns = [ path("", home, name="home"), path("log-attendance/", log_attendance, name="log_attendance"), path("work-logs/", work_log_list, name="work_log_list"), + path("work-logs/export/", export_work_log_csv, name="export_work_log_csv"), path("manage-resources/", manage_resources, name="manage_resources"), path("manage-resources/toggle///", toggle_resource_status, name="toggle_resource_status"), path("payroll/", payroll_dashboard, name="payroll_dashboard"), diff --git a/core/views.py b/core/views.py index 956a7db..f4d7f32 100644 --- a/core/views.py +++ b/core/views.py @@ -1,6 +1,7 @@ import os import platform import json +import csv from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone from django.contrib.auth.decorators import login_required @@ -8,7 +9,7 @@ from django.db.models import Sum, Q, Prefetch from django.core.mail import send_mail from django.conf import settings from django.contrib import messages -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse from .models import Worker, Project, Team, WorkLog, PayrollRecord from .forms import WorkLogForm from datetime import timedelta @@ -156,8 +157,11 @@ def work_log_list(request): logs = WorkLog.objects.all().prefetch_related('workers', 'project', 'supervisor', '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: # Find workers in this team and filter logs containing them @@ -180,9 +184,23 @@ def work_log_list(request): else: logs = logs.filter(paid_in__isnull=True) + # Calculate amounts for display + # Convert to list to attach attributes + final_logs = [] + total_amount = 0 + for log in logs: + if target_worker: + log.display_amount = target_worker.day_rate + else: + # Sum of all workers in this log + log.display_amount = sum(w.day_rate for w in log.workers.all()) + final_logs.append(log) + total_amount += log.display_amount + # Context for filters context = { - 'logs': logs, + 'logs': final_logs, + 'total_amount': total_amount, 'workers': Worker.objects.filter(is_active=True).order_by('name'), 'teams': Team.objects.filter(is_active=True).order_by('name'), 'projects': Project.objects.filter(is_active=True).order_by('name'), @@ -190,10 +208,71 @@ def work_log_list(request): 'selected_team': int(team_id) if team_id else None, 'selected_project': int(project_id) if project_id else None, 'selected_payment_status': payment_status, + 'target_worker': target_worker, } return render(request, 'core/work_log_list.html', context) +def export_work_log_csv(request): + """Export filtered work logs to CSV.""" + worker_id = request.GET.get('worker') + team_id = request.GET.get('team') + project_id = request.GET.get('project') + payment_status = request.GET.get('payment_status') + + logs = WorkLog.objects.all().prefetch_related('workers', 'project', 'supervisor', 'paid_in').order_by('-date', '-id') + + target_worker = None + if worker_id: + logs = logs.filter(workers__id=worker_id) + target_worker = Worker.objects.filter(id=worker_id).first() + + if team_id: + team_workers = Worker.objects.filter(teams__id=team_id) + logs = logs.filter(workers__in=team_workers).distinct() + + if project_id: + logs = logs.filter(project_id=project_id) + + if payment_status == 'paid': + logs = logs.filter(paid_in__isnull=False).distinct() + elif payment_status == 'unpaid': + 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) + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="work_logs.csv"' + + writer = csv.writer(response) + writer.writerow(['Date', 'Project', 'Workers', 'Amount', 'Payment Status', 'Supervisor']) + + for log in logs: + # Amount Logic + if target_worker: + display_amount = target_worker.day_rate + workers_str = target_worker.name + else: + display_amount = sum(w.day_rate for w in log.workers.all()) + workers_str = ", ".join([w.name for w in log.workers.all()]) + + # Payment Status Logic + is_paid = log.paid_in.exists() + status_str = "Paid" if is_paid else "Pending" + + writer.writerow([ + log.date, + log.project.name, + workers_str, + f"{display_amount:.2f}", + status_str, + log.supervisor.username if log.supervisor else "System" + ]) + + return response + def manage_resources(request): """View to manage active status of resources.""" # Prefetch teams for workers to avoid N+1 in template @@ -227,7 +306,7 @@ def toggle_resource_status(request, model_type, pk): return JsonResponse({ 'success': True, 'is_active': obj.is_active, - 'message': f"{obj.name} is now {'Active' if obj.is_active else 'Inactive'}." + 'message': f"{obj.name} is now {'Active' if obj.is_active else 'Inactive'}ணை." }) return redirect('manage_resources') @@ -346,4 +425,4 @@ def payslip_detail(request, pk): 'record': record, 'logs': logs, } - return render(request, 'core/payslip.html', context) \ No newline at end of file + return render(request, 'core/payslip.html', context)