ver 4
This commit is contained in:
parent
bc608d7d1e
commit
bcf2bc58dd
Binary file not shown.
@ -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'
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
26
core/migrations/0004_payrollrecord.py
Normal file
26
core/migrations/0004_payrollrecord.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0004_payrollrecord.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0004_payrollrecord.cpython-311.pyc
Normal file
Binary file not shown.
@ -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}"
|
||||||
|
|||||||
@ -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">
|
||||||
© 2026 LabourFlow Management System. All rights reserved.
|
© 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>
|
||||||
@ -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 →</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
|
||||||
|
|||||||
175
core/templates/core/manage_resources.html
Normal file
175
core/templates/core/manage_resources.html
Normal 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 %}
|
||||||
175
core/templates/core/payroll_dashboard.html
Normal file
175
core/templates/core/payroll_dashboard.html
Normal 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 %}
|
||||||
82
core/templates/core/payslip.html
Normal file
82
core/templates/core/payslip.html
Normal 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 © 2026</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
16
core/urls.py
16
core/urls.py
@ -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"),
|
||||||
]
|
]
|
||||||
188
core/views.py
188
core/views.py
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user