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.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'core',
]
@ -179,4 +180,4 @@ if EMAIL_USE_SSL:
# Default primary key field type
# 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 decimal import Decimal
from django.contrib.auth.models import User
from django.utils import timezone
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
@ -54,4 +55,14 @@ class WorkLog(models.Model):
notes = models.TextField(blank=True)
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>
<meta charset="UTF-8">
<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 %}
<meta name="description" content="{{ project_description }}">
{% endif %}
@ -23,7 +23,7 @@
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #0f172a;">
<div class="container">
<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>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<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 '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 '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>
</ul>
</div>
@ -40,15 +42,26 @@
</nav>
<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 %}
</main>
<footer class="py-4 mt-5 border-top bg-light">
<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>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
</html>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% load static %}
{% load static humanize %}
{% block title %}Dashboard | LabourFlow{% endblock %}
{% block title %}Dashboard | Fox Fitt{% endblock %}
{% block content %}
<div class="dashboard-header">
@ -21,43 +21,44 @@
</div>
<div class="container mb-5">
<div class="row g-4 mb-5">
<!-- Stats Cards -->
<!-- Payroll Analytics Row -->
<div class="row g-4 mb-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>
<p class="text-uppercase small fw-bold mb-1 opacity-75">Active Workers</p>
<h2 class="mb-0">{{ workers_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="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>
</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 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"><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>
@ -66,6 +67,34 @@
<div class="row">
<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="d-flex justify-content-between align-items-center mb-4">
<h3 class="mb-0">Recent Daily Logs</h3>
@ -109,6 +138,9 @@
<div class="card p-4">
<h3 class="mb-4">Quick Links</h3>
<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/">
<span class="me-2">👷</span> Manage Workers
</a>
@ -118,9 +150,6 @@
<a class="sidebar-link" href="/admin/core/team/">
<span class="me-2">👥</span> Manage Teams
</a>
<a class="sidebar-link" href="{% url 'work_log_list' %}">
<span class="me-2">📅</span> View All Logs
</a>
<hr>
<a class="sidebar-link text-primary fw-bold" href="/admin/core/worker/add/">
+ 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 .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 = [
path("", home, name="home"),
path("log-attendance/", log_attendance, name="log_attendance"),
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.utils import timezone
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 datetime import timedelta
def home(request):
"""Render the landing screen with dashboard stats."""
@ -13,6 +18,36 @@ def home(request):
projects_count = Project.objects.count()
teams_count = Team.objects.count()
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 = {
"workers_count": workers_count,
@ -20,6 +55,9 @@ def home(request):
"teams_count": teams_count,
"recent_logs": recent_logs,
"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)
@ -56,4 +94,150 @@ def log_attendance(request):
def work_log_list(request):
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)