This commit is contained in:
Flatlogic Bot 2026-02-03 20:11:39 +00:00
parent bc608d7d1e
commit bcf2bc58dd
15 changed files with 755 additions and 45 deletions

View File

@ -55,6 +55,7 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.humanize',
'core', 'core',
] ]
@ -179,4 +180,4 @@ if EMAIL_USE_SSL:
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -0,0 +1,26 @@
# Generated by Django 5.2.7 on 2026-02-03 17:44
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_project_is_active_team_is_active_worker_is_active'),
]
operations = [
migrations.CreateModel(
name='PayrollRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=django.utils.timezone.now)),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('work_logs', models.ManyToManyField(related_name='paid_in', to='core.worklog')),
('worker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payroll_records', to='core.worker')),
],
),
]

View File

@ -2,6 +2,7 @@ from django.db import models
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from decimal import Decimal from decimal import Decimal
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
@ -54,4 +55,14 @@ class WorkLog(models.Model):
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
def __str__(self): def __str__(self):
return f"{self.date} - {self.project.name}" return f"{self.date} - {self.project.name}"
class PayrollRecord(models.Model):
worker = models.ForeignKey(Worker, on_delete=models.CASCADE, related_name='payroll_records')
date = models.DateField(default=timezone.now)
amount = models.DecimalField(max_digits=10, decimal_places=2)
work_logs = models.ManyToManyField(WorkLog, related_name='paid_in')
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Payment to {self.worker.name} on {self.date}"

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}LabourFlow - Work & Payroll{% endblock %}</title> <title>{% block title %}Fox Fitt - Work & Payroll{% endblock %}</title>
{% if project_description %} {% if project_description %}
<meta name="description" content="{{ project_description }}"> <meta name="description" content="{{ project_description }}">
{% endif %} {% endif %}
@ -23,7 +23,7 @@
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #0f172a;"> <nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #0f172a;">
<div class="container"> <div class="container">
<a class="navbar-brand heading-font fw-bold" href="{% url 'home' %}"> <a class="navbar-brand heading-font fw-bold" href="{% url 'home' %}">
<span style="color: #10b981;">Labour</span>Flow <span style="color: #10b981;">Fox</span> Fitt
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -33,6 +33,8 @@
<li class="nav-item"><a class="nav-link" href="{% url 'home' %}">Dashboard</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'home' %}">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'log_attendance' %}">Log Work</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'log_attendance' %}">Log Work</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'work_log_list' %}">History</a></li> <li class="nav-item"><a class="nav-link" href="{% url 'work_log_list' %}">History</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'payroll_dashboard' %}">Payroll</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'manage_resources' %}">Manage</a></li>
<li class="nav-item ms-lg-3"><a class="btn btn-sm btn-outline-light" href="/admin/">Admin Panel</a></li> <li class="nav-item ms-lg-3"><a class="btn btn-sm btn-outline-light" href="/admin/">Admin Panel</a></li>
</ul> </ul>
</div> </div>
@ -40,15 +42,26 @@
</nav> </nav>
<main> <main>
{% if messages %}
<div class="container mt-3">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer class="py-4 mt-5 border-top bg-light"> <footer class="py-4 mt-5 border-top bg-light">
<div class="container text-center text-muted small"> <div class="container text-center text-muted small">
&copy; 2026 LabourFlow Management System. All rights reserved. &copy; 2026 Fox Fitt Management System. All rights reserved.
</div> </div>
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static humanize %}
{% block title %}Dashboard | LabourFlow{% endblock %} {% block title %}Dashboard | Fox Fitt{% endblock %}
{% block content %} {% block content %}
<div class="dashboard-header"> <div class="dashboard-header">
@ -21,43 +21,44 @@
</div> </div>
<div class="container mb-5"> <div class="container mb-5">
<div class="row g-4 mb-5"> <!-- Payroll Analytics Row -->
<!-- Stats Cards --> <div class="row g-4 mb-4">
<div class="col-md-4"> <div class="col-md-4">
<div class="card stat-card p-4"> <div class="card stat-card p-4 border-start border-4 border-warning">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-uppercase small fw-bold mb-1 opacity-75">Outstanding Payments</p>
<h2 class="mb-0">R {{ outstanding_total|intcomma }}</h2>
</div>
<div class="p-3 bg-warning bg-opacity-10 rounded-circle text-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg>
</div>
</div>
<a href="{% url 'payroll_dashboard' %}" class="small text-muted mt-2 d-block stretched-link">View details &rarr;</a>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card p-4 border-start border-4 border-success">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-uppercase small fw-bold mb-1 opacity-75">Paid (Last 60 Days)</p>
<h2 class="mb-0">R {{ recent_payments_total|intcomma }}</h2>
</div>
<div class="p-3 bg-success bg-opacity-10 rounded-circle text-success">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card p-4">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<p class="text-uppercase small fw-bold mb-1 opacity-75">Active Workers</p> <p class="text-uppercase small fw-bold mb-1 opacity-75">Active Workers</p>
<h2 class="mb-0">{{ workers_count }}</h2> <h2 class="mb-0">{{ workers_count }}</h2>
</div> </div>
<div class="p-3 bg-white bg-opacity-25 rounded-circle"> <div class="p-3 bg-primary bg-opacity-10 rounded-circle text-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card p-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-uppercase small fw-bold mb-1 opacity-75">Projects</p>
<h2 class="mb-0">{{ projects_count }}</h2>
</div>
<div class="p-3 bg-white bg-opacity-25 rounded-circle">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card p-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="text-uppercase small fw-bold mb-1 opacity-75">Teams</p>
<h2 class="mb-0">{{ teams_count }}</h2>
</div>
<div class="p-3 bg-white bg-opacity-25 rounded-circle">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
</div> </div>
</div> </div>
</div> </div>
@ -66,6 +67,34 @@
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">
<!-- Project Costs -->
<div class="card p-4 mb-4">
<h3 class="mb-3">Project Costs (Active)</h3>
{% if project_costs %}
<div class="table-responsive">
<table class="table align-middle">
<thead class="table-light">
<tr>
<th>Project Name</th>
<th class="text-end">Total Labor Cost</th>
</tr>
</thead>
<tbody>
{% for p in project_costs %}
<tr>
<td class="fw-bold">{{ p.name }}</td>
<td class="text-end">R {{ p.cost|intcomma }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No cost data available for active projects.</p>
{% endif %}
</div>
<!-- Recent Logs -->
<div class="card p-4 mb-4"> <div class="card p-4 mb-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="mb-0">Recent Daily Logs</h3> <h3 class="mb-0">Recent Daily Logs</h3>
@ -109,6 +138,9 @@
<div class="card p-4"> <div class="card p-4">
<h3 class="mb-4">Quick Links</h3> <h3 class="mb-4">Quick Links</h3>
<nav class="nav flex-column"> <nav class="nav flex-column">
<a class="sidebar-link" href="{% url 'payroll_dashboard' %}">
<span class="me-2">💰</span> Payroll Dashboard
</a>
<a class="sidebar-link" href="/admin/core/worker/"> <a class="sidebar-link" href="/admin/core/worker/">
<span class="me-2">👷</span> Manage Workers <span class="me-2">👷</span> Manage Workers
</a> </a>
@ -118,9 +150,6 @@
<a class="sidebar-link" href="/admin/core/team/"> <a class="sidebar-link" href="/admin/core/team/">
<span class="me-2">👥</span> Manage Teams <span class="me-2">👥</span> Manage Teams
</a> </a>
<a class="sidebar-link" href="{% url 'work_log_list' %}">
<span class="me-2">📅</span> View All Logs
</a>
<hr> <hr>
<a class="sidebar-link text-primary fw-bold" href="/admin/core/worker/add/"> <a class="sidebar-link text-primary fw-bold" href="/admin/core/worker/add/">
+ Add New Worker + Add New Worker

View File

@ -0,0 +1,175 @@
{% extends "base.html" %}
{% block title %}Manage Resources - LabourFlow{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row mb-4">
<div class="col">
<h1 class="fw-bold text-dark">Manage Resources</h1>
<p class="text-muted">Toggle the active status of workers, projects, and teams. Inactive items will be hidden from selection forms.</p>
</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-4" id="resourceTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="workers-tab" data-bs-toggle="tab" data-bs-target="#workers" type="button" role="tab">Workers</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="projects-tab" data-bs-toggle="tab" data-bs-target="#projects" type="button" role="tab">Projects</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="teams-tab" data-bs-toggle="tab" data-bs-target="#teams" type="button" role="tab">Teams</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content" id="resourceTabsContent">
<!-- Workers Tab -->
<div class="tab-pane fade show active" id="workers" role="tabpanel">
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Name</th>
<th>ID Number</th>
<th>Status</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
{% for worker in workers %}
<tr>
<td>{{ worker.name }}</td>
<td>{{ worker.id_no }}</td>
<td>
{% if worker.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td class="text-end">
<form method="post" action="{% url 'toggle_resource_status' 'worker' worker.id %}">
{% csrf_token %}
{% if worker.is_active %}
<button type="submit" class="btn btn-sm btn-outline-danger">Deactivate</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-success">Activate</button>
{% endif %}
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-4">No workers found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Projects Tab -->
<div class="tab-pane fade" id="projects" role="tabpanel">
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Description</th>
<th>Status</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr>
<td>{{ project.name }}</td>
<td>{{ project.description|truncatechars:50 }}</td>
<td>
{% if project.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td class="text-end">
<form method="post" action="{% url 'toggle_resource_status' 'project' project.id %}">
{% csrf_token %}
{% if project.is_active %}
<button type="submit" class="btn btn-sm btn-outline-danger">Deactivate</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-success">Activate</button>
{% endif %}
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-4">No projects found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Teams Tab -->
<div class="tab-pane fade" id="teams" role="tabpanel">
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Supervisor</th>
<th>Workers Count</th>
<th>Status</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ team.name }}</td>
<td>{{ team.supervisor.username|default:"-" }}</td>
<td>{{ team.workers.count }}</td>
<td>
{% if team.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td class="text-end">
<form method="post" action="{% url 'toggle_resource_status' 'team' team.id %}">
{% csrf_token %}
{% if team.is_active %}
<button type="submit" class="btn btn-sm btn-outline-danger">Deactivate</button>
{% else %}
<button type="submit" class="btn btn-sm btn-outline-success">Activate</button>
{% endif %}
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">No teams found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,175 @@
{% extends 'base.html' %}
{% load humanize %}
{% block title %}Payroll Dashboard - Fox Fitt{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 fw-bold text-dark">Payroll Dashboard</h1>
</div>
<!-- Analytics Section -->
<div class="row mb-5">
<!-- Outstanding Payments -->
<div class="col-md-4 mb-3">
<div class="card border-0 shadow-sm h-100 bg-warning bg-opacity-10">
<div class="card-body">
<h6 class="text-uppercase text-muted fw-bold small">Outstanding Payments</h6>
<div class="display-6 fw-bold text-dark">R {{ outstanding_total|intcomma }}</div>
<p class="text-muted small mb-0">Total pending for active workers</p>
</div>
</div>
</div>
<!-- Recent Payments -->
<div class="col-md-4 mb-3">
<div class="card border-0 shadow-sm h-100 bg-success bg-opacity-10">
<div class="card-body">
<h6 class="text-uppercase text-muted fw-bold small">Recent Payments (2 Months)</h6>
<div class="display-6 fw-bold text-dark">R {{ recent_payments_total|intcomma }}</div>
<p class="text-muted small mb-0">Total paid out</p>
</div>
</div>
</div>
<!-- Project Costs -->
<div class="col-md-4 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white fw-bold small text-uppercase text-muted">Project Costs (Active)</div>
<div class="card-body overflow-auto" style="max-height: 150px;">
{% if project_costs %}
<ul class="list-group list-group-flush">
{% for p in project_costs %}
<li class="list-group-item d-flex justify-content-between align-items-center px-0 py-2">
<span>{{ p.name }}</span>
<span class="fw-bold text-dark">R {{ p.cost|intcomma }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted small mb-0">No active project costs.</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Filter Tabs -->
<ul class="nav nav-pills mb-4">
<li class="nav-item">
<a class="nav-link {% if active_tab == 'pending' %}active{% endif %}" href="?status=pending">Pending Payments</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_tab == 'paid' %}active{% endif %}" href="?status=paid">Payment History</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_tab == 'all' %}active{% endif %}" href="?status=all">All Records</a>
</li>
</ul>
<!-- Pending Payments Table -->
{% if active_tab == 'pending' or active_tab == 'all' %}
{% if workers_data %}
<div class="card border-0 shadow-sm mb-5">
<div class="card-header bg-white fw-bold">Pending Payments</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Worker Name</th>
<th>Unpaid Logs</th>
<th>Total Owed</th>
<th class="text-end pe-4">Action</th>
</tr>
</thead>
<tbody>
{% for item in workers_data %}
<tr>
<td class="ps-4">
<div class="fw-bold text-dark">{{ item.worker.name }}</div>
<div class="small text-muted">ID: {{ item.worker.id_no }}</div>
</td>
<td>
<span class="badge bg-secondary">{{ item.unpaid_count }} days</span>
</td>
<td class="fw-bold text-success">
R {{ item.unpaid_amount|intcomma }}
</td>
<td class="text-end pe-4">
<form action="{% url 'process_payment' item.worker.id %}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('Confirm payment of R {{ item.unpaid_amount }} to {{ item.worker.name }}? This will email the receipt to accounting.')">
Pay Now
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% elif active_tab == 'pending' %}
<div class="alert alert-info shadow-sm mb-4">
<div class="d-flex align-items-center">
<div class="fs-4 me-3">🎉</div>
<div>
<strong>All caught up!</strong><br>
There are no outstanding payments for active workers.
</div>
</div>
</div>
{% endif %}
{% endif %}
<!-- Payment History Table -->
{% if active_tab == 'paid' or active_tab == 'all' %}
{% if paid_records %}
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-bold">Payment History</div>
<div class="card-body p-0">
<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>Payslip ID</th>
<th>Worker</th>
<th>Amount</th>
<th class="text-end pe-4">Action</th>
</tr>
</thead>
<tbody>
{% for record in paid_records %}
<tr>
<td class="ps-4">{{ record.date|date:"M d, Y" }}</td>
<td class="text-muted">#{{ record.id|stringformat:"06d" }}</td>
<td>
<div class="fw-bold">{{ record.worker.name }}</div>
<div class="small text-muted">{{ record.worker.id_no }}</div>
</td>
<td class="fw-bold">R {{ record.amount|intcomma }}</td>
<td class="text-end pe-4">
<a href="{% url 'payslip_detail' record.id %}" class="btn btn-sm btn-outline-primary">
View Payslip
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="alert alert-light border shadow-sm">
No payment history found.
</div>
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,82 @@
{% extends 'base.html' %}
{% load humanize %}
{% block title %}Payslip #{{ record.id }} - Fox Fitt{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-print-none mb-4">
<a href="{% url 'payroll_dashboard' %}" class="btn btn-outline-secondary me-2">← Back to Payroll</a>
<button onclick="window.print()" class="btn btn-primary">Print Payslip</button>
</div>
<div class="card border-0 shadow-lg" id="payslip-card">
<div class="card-body p-5">
<!-- Header -->
<div class="row mb-5 border-bottom pb-4">
<div class="col-6">
<h2 class="fw-bold text-success mb-1">Fox Fitt</h2>
<p class="text-muted small">Construction Management System</p>
</div>
<div class="col-6 text-end">
<h3 class="fw-bold text-uppercase text-secondary">Payslip</h3>
<div class="text-muted">No. #{{ record.id|stringformat:"06d" }}</div>
<div class="fw-bold">{{ record.date|date:"F j, Y" }}</div>
</div>
</div>
<!-- Worker Details -->
<div class="row mb-5">
<div class="col-md-6">
<h6 class="text-uppercase text-muted fw-bold small mb-3">Pay To:</h6>
<h4 class="fw-bold">{{ record.worker.name }}</h4>
<p class="mb-0">ID Number: <strong>{{ record.worker.id_no }}</strong></p>
<p class="mb-0">Phone: {{ record.worker.phone_no }}</p>
</div>
<div class="col-md-6 text-end">
<h6 class="text-uppercase text-muted fw-bold small mb-3">Payment Summary:</h6>
<div class="display-6 fw-bold text-dark">R {{ record.amount|intcomma }}</div>
<p class="text-success small fw-bold">PAID</p>
</div>
</div>
<!-- Work Details -->
<h6 class="text-uppercase text-muted fw-bold small mb-3">Work Log Details</h6>
<div class="table-responsive mb-4">
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Project</th>
<th>Notes</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.date|date:"M d, Y" }}</td>
<td>{{ log.project.name }}</td>
<td>{{ log.notes|default:"-"|truncatechars:50 }}</td>
<td class="text-end">R {{ record.worker.day_rate|intcomma }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="3" class="text-end fw-bold">Total</td>
<td class="text-end fw-bold">R {{ record.amount|intcomma }}</td>
</tr>
</tfoot>
</table>
</div>
<!-- Footer -->
<div class="text-center text-muted small mt-5 pt-4 border-top">
<p>This is a computer-generated document and does not require a signature.</p>
<p>Fox Fitt &copy; 2026</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,8 +1,22 @@
from django.urls import path from django.urls import path
from .views import home, log_attendance, work_log_list from .views import (
home,
log_attendance,
work_log_list,
manage_resources,
toggle_resource_status,
payroll_dashboard,
process_payment,
payslip_detail
)
urlpatterns = [ urlpatterns = [
path("", home, name="home"), path("", home, name="home"),
path("log-attendance/", log_attendance, name="log_attendance"), path("log-attendance/", log_attendance, name="log_attendance"),
path("work-logs/", work_log_list, name="work_log_list"), path("work-logs/", work_log_list, name="work_log_list"),
path("manage-resources/", manage_resources, name="manage_resources"),
path("manage-resources/toggle/<str:model_type>/<int:pk>/", toggle_resource_status, name="toggle_resource_status"),
path("payroll/", payroll_dashboard, name="payroll_dashboard"),
path("payroll/pay/<int:worker_id>/", process_payment, name="process_payment"),
path("payroll/payslip/<int:pk>/", payslip_detail, name="payslip_detail"),
] ]

View File

@ -4,8 +4,13 @@ import json
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from .models import Worker, Project, Team, WorkLog from django.db.models import Sum, Q
from django.core.mail import send_mail
from django.conf import settings
from django.contrib import messages
from .models import Worker, Project, Team, WorkLog, PayrollRecord
from .forms import WorkLogForm from .forms import WorkLogForm
from datetime import timedelta
def home(request): def home(request):
"""Render the landing screen with dashboard stats.""" """Render the landing screen with dashboard stats."""
@ -13,6 +18,36 @@ def home(request):
projects_count = Project.objects.count() projects_count = Project.objects.count()
teams_count = Team.objects.count() teams_count = Team.objects.count()
recent_logs = WorkLog.objects.order_by('-date')[:5] recent_logs = WorkLog.objects.order_by('-date')[:5]
# Analytics
# 1. Outstanding Payments
outstanding_total = 0
active_workers = Worker.objects.filter(is_active=True)
for worker in active_workers:
# Find unpaid logs for this worker
unpaid_logs_count = worker.work_logs.exclude(paid_in__worker=worker).count()
outstanding_total += unpaid_logs_count * worker.day_rate
# 2. Project Costs (Active Projects)
# Calculate sum of day_rates for all workers in all logs for each project
project_costs = []
active_projects = Project.objects.filter(is_active=True)
# Simple iteration for calculation (safer than complex annotations given properties)
for project in active_projects:
cost = 0
logs = project.logs.all()
for log in logs:
# We need to sum the day_rate of all workers in this log
# Optimization: prefetch workers if slow, but for now just iterate
for worker in log.workers.all():
cost += worker.day_rate
if cost > 0:
project_costs.append({'name': project.name, 'cost': cost})
# 3. Previous 2 months payments
two_months_ago = timezone.now().date() - timedelta(days=60)
recent_payments_total = PayrollRecord.objects.filter(date__gte=two_months_ago).aggregate(total=Sum('amount'))['total'] or 0
context = { context = {
"workers_count": workers_count, "workers_count": workers_count,
@ -20,6 +55,9 @@ def home(request):
"teams_count": teams_count, "teams_count": teams_count,
"recent_logs": recent_logs, "recent_logs": recent_logs,
"current_time": timezone.now(), "current_time": timezone.now(),
"outstanding_total": outstanding_total,
"project_costs": project_costs,
"recent_payments_total": recent_payments_total,
} }
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
@ -56,4 +94,150 @@ def log_attendance(request):
def work_log_list(request): def work_log_list(request):
logs = WorkLog.objects.all().order_by('-date') logs = WorkLog.objects.all().order_by('-date')
return render(request, 'core/work_log_list.html', {'logs': logs}) return render(request, 'core/work_log_list.html', {'logs': logs})
def manage_resources(request):
"""View to manage active status of resources."""
workers = Worker.objects.all().order_by('name')
projects = Project.objects.all().order_by('name')
teams = Team.objects.all().order_by('name')
context = {
'workers': workers,
'projects': projects,
'teams': teams,
}
return render(request, 'core/manage_resources.html', context)
def toggle_resource_status(request, model_type, pk):
"""Toggle the is_active status of a resource."""
if request.method == 'POST':
model_map = {
'worker': Worker,
'project': Project,
'team': Team,
}
model_class = model_map.get(model_type)
if model_class:
obj = get_object_or_404(model_class, pk=pk)
obj.is_active = not obj.is_active
obj.save()
return redirect('manage_resources')
def payroll_dashboard(request):
"""Dashboard for payroll management with filtering."""
status_filter = request.GET.get('status', 'pending') # pending, paid, all
# Common Analytics
outstanding_total = 0
active_workers = Worker.objects.filter(is_active=True).order_by('name')
workers_data = [] # For pending payments
for worker in active_workers:
unpaid_logs = worker.work_logs.exclude(paid_in__worker=worker)
count = unpaid_logs.count()
amount = count * worker.day_rate
if count > 0:
outstanding_total += amount
if status_filter in ['pending', 'all']:
workers_data.append({
'worker': worker,
'unpaid_count': count,
'unpaid_amount': amount,
'logs': unpaid_logs
})
# Paid History
paid_records = []
if status_filter in ['paid', 'all']:
paid_records = PayrollRecord.objects.select_related('worker').order_by('-date', '-id')
# Analytics: Project Costs (Active Projects)
project_costs = []
active_projects = Project.objects.filter(is_active=True)
for project in active_projects:
cost = 0
logs = project.logs.all()
for log in logs:
for worker in log.workers.all():
cost += worker.day_rate
if cost > 0:
project_costs.append({'name': project.name, 'cost': cost})
# Analytics: Previous 2 months payments
two_months_ago = timezone.now().date() - timedelta(days=60)
recent_payments_total = PayrollRecord.objects.filter(date__gte=two_months_ago).aggregate(total=Sum('amount'))['total'] or 0
context = {
'workers_data': workers_data,
'paid_records': paid_records,
'outstanding_total': outstanding_total,
'project_costs': project_costs,
'recent_payments_total': recent_payments_total,
'active_tab': status_filter,
}
return render(request, 'core/payroll_dashboard.html', context)
def process_payment(request, worker_id):
"""Process payment for a worker, mark logs as paid, and email receipt."""
worker = get_object_or_404(Worker, pk=worker_id)
if request.method == 'POST':
# Find unpaid logs
unpaid_logs = worker.work_logs.exclude(paid_in__worker=worker)
count = unpaid_logs.count()
if count > 0:
amount = count * worker.day_rate
# Create Payroll Record
payroll_record = PayrollRecord.objects.create(
worker=worker,
amount=amount,
date=timezone.now().date()
)
# Link logs
payroll_record.work_logs.set(unpaid_logs)
payroll_record.save()
# Email Notification
subject = f"Payslip for {worker.name} - {payroll_record.date}"
message = (
f"Payslip Generated\n\n"
f"Record ID: #{payroll_record.id}\n"
f"Worker: {worker.name}\n"
f"ID Number: {worker.id_no}\n"
f"Date: {payroll_record.date}\n"
f"Amount Paid: R {payroll_record.amount}\n\n"
f"This is an automated notification from Fox Fitt Payroll."
)
recipient_list = ['foxfitt-ed9wc+expense@to.sparkreceipt.com']
try:
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list)
messages.success(request, f"Payment of R {payroll_record.amount} processed for {worker.name}. Email sent to accounting.")
except Exception as e:
messages.warning(request, f"Payment processed, but email delivery failed: {str(e)}")
return redirect('payroll_dashboard')
return redirect('payroll_dashboard')
def payslip_detail(request, pk):
"""Show details of a payslip (Payment Record)."""
record = get_object_or_404(PayrollRecord, pk=pk)
# Get the logs included in this payment
logs = record.work_logs.all().order_by('date')
context = {
'record': record,
'logs': logs,
}
return render(request, 'core/payslip.html', context)