|
- {{ 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 %}
|
{% if is_admin_user %}
- 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 %}
|
{% endif %}
- {{ log.supervisor.username|default:"System" }}
+ {{ record.supervisor }}
|
@@ -273,10 +282,15 @@
@@ -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 += '| Date | ';
- html += 'Project | Team(s) | Workers | Supervisor | Notes | ';
+ html += 'Description / Project | Workers | Amount | Supervisor | Notes | ';
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 += '| ' + dateLabel + ' | ';
+ html += '' + dateLabel + ' | ';
}
}
- html += '' + log.project + ' | ';
- html += '' + teams + ' | ';
+ html += '' + desc + ' | ';
html += '' + workers + ' | ';
- html += '' + log.supervisor + ' | ';
+ html += '' + amount + ' | ';
+ html += '' + rec.supervisor + ' | ';
html += '' + notes + ' | ';
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
|