Ver 14.03 Adjustments added to work log
This commit is contained in:
parent
8724b9d7bb
commit
df4cadf024
Binary file not shown.
@ -9,7 +9,7 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="display-5 mb-2">Work Log History</h1>
|
||||
<p class="lead opacity-75">Filter and review historical daily work logs.</p>
|
||||
<p class="lead opacity-75">Filter and review historical daily work logs and adjustments.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<!-- View Switcher -->
|
||||
@ -142,31 +142,31 @@
|
||||
{% for week in calendar_weeks %}
|
||||
<tr>
|
||||
{% for day_info in week %}
|
||||
<td class="cal-day {% if day_info.logs %}cal-day--has-logs{% endif %} {% if not day_info.is_current_month %}bg-light text-muted bg-opacity-10{% endif %}"
|
||||
<td class="cal-day {% if day_info.records %}cal-day--has-logs{% endif %} {% if not day_info.is_current_month %}bg-light text-muted bg-opacity-10{% endif %}"
|
||||
data-date="{{ day_info.date|date:'Y-m-d' }}"
|
||||
style="height: 140px; vertical-align: top; padding: 10px;">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<span class="fw-bold {% if day_info.date == current_time.date %}text-primary bg-primary bg-opacity-10 px-2 rounded-circle{% endif %}">{{ day_info.day }}</span>
|
||||
{% if day_info.logs %}
|
||||
<span class="badge bg-secondary opacity-50">{{ day_info.logs|length }}</span>
|
||||
{% if day_info.records %}
|
||||
<span class="badge bg-secondary opacity-50">{{ day_info.records|length }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="calendar-events" style="max-height: 100px; overflow-y: auto;">
|
||||
{% for log in day_info.logs %}
|
||||
<div class="mb-1 p-1 rounded border small bg-white shadow-sm" style="font-size: 0.75rem; line-height: 1.2; border-left: 3px solid var(--bs-primary) !important;">
|
||||
<div class="fw-bold text-truncate">{{ log.project.name }}</div>
|
||||
{% if log.team %}
|
||||
{% for record in day_info.records %}
|
||||
<div class="mb-1 p-1 rounded border small bg-white shadow-sm" style="font-size: 0.75rem; line-height: 1.2; border-left: 3px solid {% if record.type == 'ADJ' %}#dc3545{% else %}var(--bs-primary){% endif %} !important;">
|
||||
<div class="fw-bold text-truncate">{{ record.project_name }}</div>
|
||||
{% if record.type == 'WORK' and record.team_name %}
|
||||
<div class="text-muted small text-truncate" style="font-size: 0.7rem;">
|
||||
<i class="bi bi-people-fill me-1"></i>{{ log.team.name }}
|
||||
<i class="bi bi-people-fill me-1"></i>{{ record.team_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="text-muted text-truncate" title="{{ log.workers.count }} workers">
|
||||
<div class="text-muted text-truncate">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
@ -196,14 +196,14 @@
|
||||
<!-- LIST VIEW -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-0">
|
||||
{% if logs %}
|
||||
{% if records %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Date</th>
|
||||
<th>Project</th>
|
||||
<th>Labourers</th>
|
||||
<th>Description / Project</th>
|
||||
<th>Worker(s)</th>
|
||||
{% if is_admin_user %}<th>Amount</th>{% endif %}
|
||||
{% if is_admin_user %}<th>Status / Payslip</th>{% endif %}
|
||||
<th>Supervisor</th>
|
||||
@ -211,61 +211,70 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
{% for record in records %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<span class="fw-bold">{{ log.date|date:"D, d M Y" }}</span>
|
||||
<span class="fw-bold">{{ record.date|date:"D, d M Y" }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 px-2 py-1">
|
||||
{{ log.project.name }}
|
||||
</span>
|
||||
{% if record.type == 'WORK' %}
|
||||
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 px-2 py-1">
|
||||
{{ record.project_name }}
|
||||
</span>
|
||||
{% if record.team_name %}
|
||||
<small class="text-muted ms-1"><i class="bi bi-people-fill"></i> {{ record.team_name }}</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25 px-2 py-1">
|
||||
{{ record.project_name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if selected_worker %}
|
||||
<span class="text-primary fw-medium">
|
||||
{% for w in log.workers.all %}
|
||||
{% for w in record.workers %}
|
||||
{% if w.id == selected_worker %}
|
||||
{{ w.name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% if log.workers.count > 1 %}
|
||||
<small class="text-muted">(+{{ log.workers.count|add:"-1" }} others)</small>
|
||||
{% if record.workers|length > 1 %}
|
||||
<small class="text-muted">(+{{ record.workers|length|add:"-1" }} others)</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for w in log.workers.all|slice:":3" %}
|
||||
{% for w in record.workers|slice:":3" %}
|
||||
<span class="small bg-light px-2 py-1 rounded border">{{ w.name|truncatechars:12 }}</span>
|
||||
{% endfor %}
|
||||
{% if log.workers.count > 3 %}
|
||||
<span class="small text-muted align-self-center ms-1">+{{ log.workers.count|add:"-3" }}</span>
|
||||
{% if record.workers|length > 3 %}
|
||||
<span class="small text-muted align-self-center ms-1">+{{ record.workers|length|add:"-3" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if is_admin_user %}
|
||||
<td>
|
||||
<span class="fw-bold font-monospace text-dark">R {{ log.display_amount|floatformat:2 }}</span>
|
||||
<span class="fw-bold font-monospace {% if record.amount < 0 %}text-danger{% else %}text-dark{% endif %}">
|
||||
R {{ record.amount|floatformat:2 }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% with payslip=log.paid_in.first %}
|
||||
{% if payslip %}
|
||||
<a href="{% url 'payslip_detail' payslip.id %}" class="text-decoration-none">
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25">
|
||||
<i class="bi bi-check-circle-fill"></i> Paid (Slip #{{ payslip.id }})
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge bg-warning bg-opacity-10 text-warning border border-warning border-opacity-25">
|
||||
Pending
|
||||
{% if record.paid_record %}
|
||||
<a href="{% url 'payslip_detail' record.paid_record.id %}" class="text-decoration-none">
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25">
|
||||
<i class="bi bi-check-circle-fill"></i> Paid (Slip #{{ record.paid_record.id }})
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge bg-warning bg-opacity-10 text-warning border border-warning border-opacity-25">
|
||||
Pending
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<small class="text-muted">{{ log.supervisor.username|default:"System" }}</small>
|
||||
<small class="text-muted">{{ record.supervisor }}</small>
|
||||
</td>
|
||||
<td class="pe-4 text-end">
|
||||
<div class="dropdown">
|
||||
@ -273,10 +282,15 @@
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-content dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="/admin/core/worklog/{{ log.id }}/change/">Edit Entry</a></li>
|
||||
{% if log.notes %}
|
||||
{% if record.type == 'WORK' %}
|
||||
<li><a class="dropdown-item" href="/admin/core/worklog/{{ record.obj.id }}/change/">Edit Entry</a></li>
|
||||
{% else %}
|
||||
<li><a class="dropdown-item" href="/admin/core/payrolladjustment/{{ record.obj.id }}/change/">Edit Adjustment</a></li>
|
||||
{% endif %}
|
||||
|
||||
{% if record.notes %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="px-3 py-1"><small class="text-muted">Note: {{ log.notes }}</small></li>
|
||||
<li class="px-3 py-1"><small class="text-muted">Note: {{ record.notes }}</small></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -298,7 +312,7 @@
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-search display-1 text-muted opacity-25 mb-3 d-block"></i>
|
||||
<h4 class="text-muted">No logs found matching filters.</h4>
|
||||
<h4 class="text-muted">No records found matching filters.</h4>
|
||||
<p class="text-muted mb-4">Try adjusting your filters or record a new entry.</p>
|
||||
<a href="{% url 'log_attendance' %}" class="btn btn-primary">Log Attendance</a>
|
||||
</div>
|
||||
@ -362,30 +376,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let html = '<div class="table-responsive"><table class="table table-hover align-middle mb-0">';
|
||||
html += '<thead class="bg-light"><tr>';
|
||||
if (sorted.length > 1) html += '<th class="ps-4">Date</th>';
|
||||
html += '<th class="' + (sorted.length === 1 ? 'ps-4' : '') + '">Project</th><th>Team(s)</th><th>Workers</th><th>Supervisor</th><th>Notes</th></tr></thead><tbody>';
|
||||
html += '<th class="' + (sorted.length === 1 ? 'ps-4' : '') + '">Description / Project</th><th>Workers</th><th>Amount</th><th>Supervisor</th><th>Notes</th></tr></thead><tbody>';
|
||||
|
||||
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(', ') : '<span class="text-muted">-</span>';
|
||||
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 = '<span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25">' + rec.project + '</span>';
|
||||
} else {
|
||||
desc = '<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25">' + rec.project + '</span>';
|
||||
if (teams) desc += ' <small class="text-muted ms-1">' + teams + '</small>';
|
||||
}
|
||||
|
||||
const workers = rec.workers.map(function(w) {
|
||||
return '<span class="small bg-light px-2 py-1 rounded border d-inline-block mb-1">' + w + '</span>';
|
||||
}).join(' ');
|
||||
const notes = log.notes ? '<small class="text-muted">' + log.notes + '</small>' : '<span class="text-muted">-</span>';
|
||||
|
||||
const notes = rec.notes ? '<small class="text-muted">' + rec.notes + '</small>' : '<span class="text-muted">-</span>';
|
||||
const amount = rec.amount ? 'R ' + rec.amount.toFixed(2) : '-';
|
||||
|
||||
html += '<tr>';
|
||||
if (sorted.length > 1) {
|
||||
if (idx === 0) {
|
||||
html += '<td class="ps-4 fw-bold" rowspan="' + logs.length + '">' + dateLabel + '</td>';
|
||||
html += '<td class="ps-4 fw-bold" rowspan="' + records.length + '">' + dateLabel + '</td>';
|
||||
}
|
||||
}
|
||||
html += '<td class="' + (sorted.length === 1 ? 'ps-4 ' : '') + 'fw-bold">' + log.project + '</td>';
|
||||
html += '<td>' + teams + '</td>';
|
||||
html += '<td class="' + (sorted.length === 1 ? 'ps-4 ' : '') + '">' + desc + '</td>';
|
||||
html += '<td>' + workers + '</td>';
|
||||
html += '<td><small class="text-muted">' + log.supervisor + '</small></td>';
|
||||
html += '<td class="font-monospace fw-bold">' + amount + '</td>';
|
||||
html += '<td><small class="text-muted">' + rec.supervisor + '</small></td>';
|
||||
html += '<td>' + notes + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
@ -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
|
||||
|
||||
218
core/views.py
218
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user