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 class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="display-5 mb-2">Work Log History</h1>
|
<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>
|
||||||
<div class="d-flex gap-2 align-items-center">
|
<div class="d-flex gap-2 align-items-center">
|
||||||
<!-- View Switcher -->
|
<!-- View Switcher -->
|
||||||
@ -142,31 +142,31 @@
|
|||||||
{% for week in calendar_weeks %}
|
{% for week in calendar_weeks %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for day_info in week %}
|
{% 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' }}"
|
data-date="{{ day_info.date|date:'Y-m-d' }}"
|
||||||
style="height: 140px; vertical-align: top; padding: 10px;">
|
style="height: 140px; vertical-align: top; padding: 10px;">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
<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>
|
<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 %}
|
{% if day_info.records %}
|
||||||
<span class="badge bg-secondary opacity-50">{{ day_info.logs|length }}</span>
|
<span class="badge bg-secondary opacity-50">{{ day_info.records|length }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="calendar-events" style="max-height: 100px; overflow-y: auto;">
|
<div class="calendar-events" style="max-height: 100px; overflow-y: auto;">
|
||||||
{% for log in day_info.logs %}
|
{% 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 var(--bs-primary) !important;">
|
<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">{{ log.project.name }}</div>
|
<div class="fw-bold text-truncate">{{ record.project_name }}</div>
|
||||||
{% if log.team %}
|
{% if record.type == 'WORK' and record.team_name %}
|
||||||
<div class="text-muted small text-truncate" style="font-size: 0.7rem;">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="text-muted text-truncate" title="{{ log.workers.count }} workers">
|
<div class="text-muted text-truncate">
|
||||||
{% if selected_worker %}
|
{% if selected_worker %}
|
||||||
{{ log.workers.first.name }}
|
{{ record.workers.0.name }}
|
||||||
{% elif log.workers.count == 1 %}
|
{% elif record.workers|length == 1 %}
|
||||||
{{ log.workers.first.name }}
|
{{ record.workers.0.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ log.workers.count }} workers
|
{{ record.workers|length }} workers
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,14 +196,14 @@
|
|||||||
<!-- LIST VIEW -->
|
<!-- LIST VIEW -->
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm border-0">
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{% if logs %}
|
{% if records %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="ps-4">Date</th>
|
<th class="ps-4">Date</th>
|
||||||
<th>Project</th>
|
<th>Description / Project</th>
|
||||||
<th>Labourers</th>
|
<th>Worker(s)</th>
|
||||||
{% if is_admin_user %}<th>Amount</th>{% endif %}
|
{% if is_admin_user %}<th>Amount</th>{% endif %}
|
||||||
{% if is_admin_user %}<th>Status / Payslip</th>{% endif %}
|
{% if is_admin_user %}<th>Status / Payslip</th>{% endif %}
|
||||||
<th>Supervisor</th>
|
<th>Supervisor</th>
|
||||||
@ -211,61 +211,70 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for log in logs %}
|
{% for record in records %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-4">
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 px-2 py-1">
|
{% if record.type == 'WORK' %}
|
||||||
{{ log.project.name }}
|
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 px-2 py-1">
|
||||||
</span>
|
{{ 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>
|
||||||
<td>
|
<td>
|
||||||
{% if selected_worker %}
|
{% if selected_worker %}
|
||||||
<span class="text-primary fw-medium">
|
<span class="text-primary fw-medium">
|
||||||
{% for w in log.workers.all %}
|
{% for w in record.workers %}
|
||||||
{% if w.id == selected_worker %}
|
{% if w.id == selected_worker %}
|
||||||
{{ w.name }}
|
{{ w.name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</span>
|
</span>
|
||||||
{% if log.workers.count > 1 %}
|
{% if record.workers|length > 1 %}
|
||||||
<small class="text-muted">(+{{ log.workers.count|add:"-1" }} others)</small>
|
<small class="text-muted">(+{{ record.workers|length|add:"-1" }} others)</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="d-flex flex-wrap gap-1">
|
<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>
|
<span class="small bg-light px-2 py-1 rounded border">{{ w.name|truncatechars:12 }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if log.workers.count > 3 %}
|
{% if record.workers|length > 3 %}
|
||||||
<span class="small text-muted align-self-center ms-1">+{{ log.workers.count|add:"-3" }}</span>
|
<span class="small text-muted align-self-center ms-1">+{{ record.workers|length|add:"-3" }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if is_admin_user %}
|
{% if is_admin_user %}
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
{% with payslip=log.paid_in.first %}
|
{% if record.paid_record %}
|
||||||
{% if payslip %}
|
<a href="{% url 'payslip_detail' record.paid_record.id %}" class="text-decoration-none">
|
||||||
<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">
|
||||||
<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 }})
|
||||||
<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
|
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
</a>
|
||||||
{% endwith %}
|
{% else %}
|
||||||
|
<span class="badge bg-warning bg-opacity-10 text-warning border border-warning border-opacity-25">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
<small class="text-muted">{{ log.supervisor.username|default:"System" }}</small>
|
<small class="text-muted">{{ record.supervisor }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="pe-4 text-end">
|
<td class="pe-4 text-end">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
@ -273,10 +282,15 @@
|
|||||||
<i class="bi bi-three-dots-vertical"></i>
|
<i class="bi bi-three-dots-vertical"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-content dropdown-menu dropdown-menu-end">
|
<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 record.type == 'WORK' %}
|
||||||
{% if log.notes %}
|
<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><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 %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -298,7 +312,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="bi bi-search display-1 text-muted opacity-25 mb-3 d-block"></i>
|
<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>
|
<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>
|
<a href="{% url 'log_attendance' %}" class="btn btn-primary">Log Attendance</a>
|
||||||
</div>
|
</div>
|
||||||
@ -362,30 +376,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let html = '<div class="table-responsive"><table class="table table-hover align-middle mb-0">';
|
let html = '<div class="table-responsive"><table class="table table-hover align-middle mb-0">';
|
||||||
html += '<thead class="bg-light"><tr>';
|
html += '<thead class="bg-light"><tr>';
|
||||||
if (sorted.length > 1) html += '<th class="ps-4">Date</th>';
|
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) {
|
sorted.forEach(function(dateStr) {
|
||||||
const logs = detailData[dateStr] || [];
|
const records = detailData[dateStr] || [];
|
||||||
const d = new Date(dateStr + 'T00:00:00');
|
const d = new Date(dateStr + 'T00:00:00');
|
||||||
const dateLabel = d.toLocaleDateString('en-ZA', dateOpts);
|
const dateLabel = d.toLocaleDateString('en-ZA', dateOpts);
|
||||||
|
|
||||||
logs.forEach(function(log, idx) {
|
records.forEach(function(rec, idx) {
|
||||||
const teams = log.teams.length ? log.teams.join(', ') : '<span class="text-muted">-</span>';
|
const teams = rec.teams.length ? rec.teams.join(', ') : '';
|
||||||
const workers = log.workers.map(function(w) {
|
|
||||||
|
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>';
|
return '<span class="small bg-light px-2 py-1 rounded border d-inline-block mb-1">' + w + '</span>';
|
||||||
}).join(' ');
|
}).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>';
|
html += '<tr>';
|
||||||
if (sorted.length > 1) {
|
if (sorted.length > 1) {
|
||||||
if (idx === 0) {
|
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 class="' + (sorted.length === 1 ? 'ps-4 ' : '') + '">' + desc + '</td>';
|
||||||
html += '<td>' + teams + '</td>';
|
|
||||||
html += '<td>' + workers + '</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 += '<td>' + notes + '</td>';
|
||||||
html += '</tr>';
|
html += '</tr>';
|
||||||
});
|
});
|
||||||
@ -400,8 +425,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.querySelectorAll('.cal-day--has-logs').forEach(function(cell) {
|
document.querySelectorAll('.cal-day--has-logs').forEach(function(cell) {
|
||||||
cell.addEventListener('click', function(e) {
|
cell.addEventListener('click', function(e) {
|
||||||
const dateStr = this.dataset.date;
|
const dateStr = this.dataset.date;
|
||||||
const logs = detailData[dateStr];
|
const records = detailData[dateStr];
|
||||||
if (!logs || !logs.length) return;
|
if (!records || !records.length) return;
|
||||||
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
// Shift+click: toggle this date in multi-selection
|
// 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
|
@login_required
|
||||||
def work_log_list(request):
|
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):
|
if not is_staff_or_supervisor(request.user):
|
||||||
return redirect('log_attendance')
|
return redirect('log_attendance')
|
||||||
|
|
||||||
@ -272,12 +272,12 @@ def work_log_list(request):
|
|||||||
payment_status = request.GET.get('payment_status') # 'paid', 'unpaid', 'all'
|
payment_status = request.GET.get('payment_status') # 'paid', 'unpaid', 'all'
|
||||||
view_mode = request.GET.get('view', 'list')
|
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')
|
logs = WorkLog.objects.all().select_related('team', 'project', 'supervisor').prefetch_related('workers', 'paid_in').order_by('-date', '-id')
|
||||||
|
|
||||||
target_worker = None
|
target_worker = None
|
||||||
if worker_id:
|
if worker_id:
|
||||||
logs = logs.filter(workers__id=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()
|
target_worker = Worker.objects.filter(id=worker_id).first()
|
||||||
|
|
||||||
if team_id:
|
if team_id:
|
||||||
@ -287,24 +287,38 @@ def work_log_list(request):
|
|||||||
logs = logs.filter(project_id=project_id)
|
logs = logs.filter(project_id=project_id)
|
||||||
|
|
||||||
if payment_status == 'paid':
|
if payment_status == 'paid':
|
||||||
# Logs that are linked to at least one PayrollRecord
|
|
||||||
logs = logs.filter(paid_in__isnull=False).distinct()
|
logs = logs.filter(paid_in__isnull=False).distinct()
|
||||||
elif payment_status == 'unpaid':
|
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:
|
if worker_id:
|
||||||
worker = get_object_or_404(Worker, pk=worker_id)
|
worker = get_object_or_404(Worker, pk=worker_id)
|
||||||
logs = logs.exclude(paid_in__worker=worker)
|
logs = logs.exclude(paid_in__worker=worker)
|
||||||
else:
|
else:
|
||||||
logs = logs.filter(paid_in__isnull=True)
|
logs = logs.filter(paid_in__isnull=True)
|
||||||
|
|
||||||
# Calculate amounts for display
|
# --- 2. Fetch Adjustments ---
|
||||||
# Convert to list to attach attributes
|
# Adjustments are shown unless a Project/Team filter is active (as they don't belong to projects/teams),
|
||||||
final_logs = []
|
# OR if a specific worker is selected (then we always show their adjustments).
|
||||||
total_amount = 0
|
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':
|
if view_mode == 'calendar':
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
try:
|
try:
|
||||||
@ -314,29 +328,81 @@ def work_log_list(request):
|
|||||||
curr_year = today.year
|
curr_year = today.year
|
||||||
curr_month = today.month
|
curr_month = today.month
|
||||||
|
|
||||||
# Bounds safety
|
|
||||||
if curr_month < 1: curr_month = 1;
|
if curr_month < 1: curr_month = 1;
|
||||||
if curr_month > 12: curr_month = 12;
|
if curr_month > 12: curr_month = 12;
|
||||||
|
|
||||||
# Get range
|
|
||||||
_, num_days = calendar.monthrange(curr_year, curr_month)
|
_, num_days = calendar.monthrange(curr_year, curr_month)
|
||||||
start_date = datetime.date(curr_year, curr_month, 1)
|
start_date = datetime.date(curr_year, curr_month, 1)
|
||||||
end_date = datetime.date(curr_year, curr_month, num_days)
|
end_date = datetime.date(curr_year, curr_month, num_days)
|
||||||
|
|
||||||
logs = logs.filter(date__range=(start_date, end_date))
|
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)
|
user_is_admin = is_admin(request.user)
|
||||||
|
total_amount = 0
|
||||||
|
combined_records = []
|
||||||
|
|
||||||
|
# Process Logs
|
||||||
for log in 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 user_is_admin:
|
||||||
if target_worker:
|
if target_worker:
|
||||||
log.display_amount = target_worker.day_rate
|
amt = target_worker.day_rate
|
||||||
else:
|
else:
|
||||||
log.display_amount = sum(w.day_rate for w in log.workers.all())
|
amt = sum(w.day_rate for w in log.workers.all())
|
||||||
total_amount += log.display_amount
|
record['amount'] = amt
|
||||||
|
total_amount += amt
|
||||||
else:
|
else:
|
||||||
log.display_amount = None
|
record['amount'] = None
|
||||||
final_logs.append(log)
|
|
||||||
|
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 for filters
|
||||||
context = {
|
context = {
|
||||||
@ -355,11 +421,12 @@ def work_log_list(request):
|
|||||||
|
|
||||||
if view_mode == 'calendar':
|
if view_mode == 'calendar':
|
||||||
# Group by date for easy lookup in template
|
# Group by date for easy lookup in template
|
||||||
logs_map = {}
|
records_map = {}
|
||||||
for log in final_logs:
|
for rec in combined_records:
|
||||||
if log.date not in logs_map:
|
d = rec['date']
|
||||||
logs_map[log.date] = []
|
if d not in records_map:
|
||||||
logs_map[log.date].append(log)
|
records_map[d] = []
|
||||||
|
records_map[d].append(rec)
|
||||||
|
|
||||||
cal = calendar.Calendar(firstweekday=0) # Monday is 0
|
cal = calendar.Calendar(firstweekday=0) # Monday is 0
|
||||||
month_dates = cal.monthdatescalendar(curr_year, curr_month)
|
month_dates = cal.monthdatescalendar(curr_year, curr_month)
|
||||||
@ -373,24 +440,28 @@ def work_log_list(request):
|
|||||||
'date': d,
|
'date': d,
|
||||||
'day': d.day,
|
'day': d.day,
|
||||||
'is_current_month': d.month == curr_month,
|
'is_current_month': d.month == curr_month,
|
||||||
'logs': logs_map.get(d, [])
|
'records': records_map.get(d, [])
|
||||||
})
|
})
|
||||||
calendar_weeks.append(week_data)
|
calendar_weeks.append(week_data)
|
||||||
|
|
||||||
# Build JSON lookup for day detail panel
|
# Build JSON lookup for day detail panel
|
||||||
calendar_detail_data = {}
|
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')
|
date_str = date_key.strftime('%Y-%m-%d')
|
||||||
calendar_detail_data[date_str] = []
|
calendar_detail_data[date_str] = []
|
||||||
for log in day_logs:
|
for rec in day_records:
|
||||||
workers_list = list(log.workers.all().order_by('name'))
|
workers_list = [w.name for w in rec['workers']]
|
||||||
team_name = log.team.name if log.team else 'No Team'
|
team_name = rec['team_name'] if rec['team_name'] else ''
|
||||||
|
|
||||||
|
# Format for JS
|
||||||
calendar_detail_data[date_str].append({
|
calendar_detail_data[date_str].append({
|
||||||
'project': log.project.name,
|
'project': rec['project_name'], # This holds Type for ADJ
|
||||||
'teams': [team_name],
|
'teams': [team_name] if team_name else [],
|
||||||
'workers': [w.name for w in workers_list],
|
'workers': workers_list,
|
||||||
'supervisor': log.supervisor.username if log.supervisor else 'System',
|
'supervisor': rec['supervisor'],
|
||||||
'notes': log.notes or '',
|
'notes': rec['notes'] or '',
|
||||||
|
'type': rec['type'],
|
||||||
|
'amount': float(rec['amount']) if rec['amount'] is not None else 0
|
||||||
})
|
})
|
||||||
|
|
||||||
# Nav Links
|
# Nav Links
|
||||||
@ -409,13 +480,13 @@ def work_log_list(request):
|
|||||||
'next_year': next_month_date.year,
|
'next_year': next_month_date.year,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
context['logs'] = final_logs
|
context['records'] = combined_records
|
||||||
|
|
||||||
return render(request, 'core/work_log_list.html', context)
|
return render(request, 'core/work_log_list.html', context)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def export_work_log_csv(request):
|
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):
|
if not is_staff_or_supervisor(request.user):
|
||||||
return HttpResponse("Unauthorized", status=401)
|
return HttpResponse("Unauthorized", status=401)
|
||||||
|
|
||||||
@ -424,6 +495,7 @@ def export_work_log_csv(request):
|
|||||||
project_id = request.GET.get('project')
|
project_id = request.GET.get('project')
|
||||||
payment_status = request.GET.get('payment_status')
|
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')
|
logs = WorkLog.objects.all().prefetch_related('workers', 'workers__teams', 'project', 'supervisor', 'paid_in').order_by('-date', '-id')
|
||||||
|
|
||||||
target_worker = None
|
target_worker = None
|
||||||
@ -447,39 +519,89 @@ def export_work_log_csv(request):
|
|||||||
else:
|
else:
|
||||||
logs = logs.filter(paid_in__isnull=True)
|
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)
|
user_is_admin = is_admin(request.user)
|
||||||
|
|
||||||
response = HttpResponse(content_type='text/csv')
|
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)
|
writer = csv.writer(response)
|
||||||
if user_is_admin:
|
if user_is_admin:
|
||||||
writer.writerow(['Date', 'Project', 'Workers', 'Amount', 'Payment Status', 'Supervisor'])
|
writer.writerow(['Date', 'Description', 'Workers', 'Amount', 'Payment Status', 'Supervisor'])
|
||||||
else:
|
else:
|
||||||
writer.writerow(['Date', 'Project', 'Workers', 'Supervisor'])
|
writer.writerow(['Date', 'Description', 'Workers', 'Supervisor'])
|
||||||
|
|
||||||
|
# Combine and Sort
|
||||||
|
combined = []
|
||||||
|
|
||||||
for log in logs:
|
for log in logs:
|
||||||
if target_worker:
|
if target_worker:
|
||||||
workers_str = target_worker.name
|
workers_str = target_worker.name
|
||||||
else:
|
else:
|
||||||
workers_str = ", ".join([w.name for w in log.workers.all()])
|
workers_str = ", ".join([w.name for w in log.workers.all()])
|
||||||
|
|
||||||
|
amt = 0
|
||||||
|
is_paid = False
|
||||||
if user_is_admin:
|
if user_is_admin:
|
||||||
if target_worker:
|
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:
|
else:
|
||||||
display_amount = sum(w.day_rate for w in log.workers.all())
|
amt = sum(w.day_rate for w in log.workers.all())
|
||||||
is_paid = log.paid_in.exists()
|
is_paid = log.paid_in.exists()
|
||||||
status_str = "Paid" if is_paid else "Pending"
|
|
||||||
|
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([
|
writer.writerow([
|
||||||
log.date, log.project.name, workers_str,
|
row['date'], row['desc'], row['workers'],
|
||||||
f"{display_amount:.2f}", status_str,
|
f"{row['amount']:.2f}", row['status'], row['supervisor']
|
||||||
log.supervisor.username if log.supervisor else "System"
|
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
log.date, log.project.name, workers_str,
|
row['date'], row['desc'], row['workers'], row['supervisor']
|
||||||
log.supervisor.username if log.supervisor else "System"
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user