mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 05:22:16 +00:00
Finance dashboard (#600)
* Enable commas for float thousands separation * Add new invoice dashboard template * Add new view controller for finance dashboard * Add finance dashboard to dropdown * Update finance URLs to put dashboard at index route * Add payment methods to generated sample data * Flip 'outstanding' and 'waiting' cards on dashboard to match order in dropdown Also made them link to their respective lists and fixed low text contrast --------- Co-authored-by: FreneticScribbler <aj@aronajones.com>
This commit is contained in:
@@ -224,6 +224,8 @@ USE_L10N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
USE_THOUSAND_SEPARATOR = True
|
||||||
|
|
||||||
# Need to allow seconds as datetime-local input type spits out a time that has seconds
|
# Need to allow seconds as datetime-local input type spits out a time that has seconds
|
||||||
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S')
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class Command(BaseCommand):
|
|||||||
new_invoice.void = True
|
new_invoice.void = True
|
||||||
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
elif random.randint(0, 2) > 1: # 1 in 3 have been paid
|
||||||
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
|
models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance,
|
||||||
date=datetime.date.today())
|
date=datetime.date.today(), method=random.choice(models.Payment.METHODS)[0])
|
||||||
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
|
if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA
|
||||||
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
|
models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)),
|
||||||
nonstandard_equipment=bool(random.getrandbits(1)),
|
nonstandard_equipment=bool(random.getrandbits(1)),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
Invoices <span class="badge {% if todo == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ todo }}</span>
|
Invoices <span class="badge {% if todo == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ todo }}</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
|
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
|
||||||
|
<a class="dropdown-item" href="{% url 'invoice_dashboard' %}"><span class="fas fa-chart-line"></span> Dashboard</a>
|
||||||
{% if perms.RIGS.add_invoice %}
|
{% if perms.RIGS.add_invoice %}
|
||||||
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ waiting }}</span></a>
|
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge {% if waiting == 0 %}badge-success{% else %}badge-danger{% endif %} badge-pill">{{ waiting }}</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
105
RIGS/templates/invoice_dashboard.html
Normal file
105
RIGS/templates/invoice_dashboard.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{% extends 'base_rigs.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form method="GET" action="{% url 'invoice_dashboard' %}">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group col-md-4">
|
||||||
|
<label for="time_filter">Time Filter</label>
|
||||||
|
<select id="time_filter" name="time_filter" class="form-control">
|
||||||
|
<option value="week" {% if time_filter == 'week' %}selected{% endif %}>Last Week (7 days)</option>
|
||||||
|
<option value="month" {% if time_filter == 'month' %}selected{% endif %}>Last Month (30 days)</option>
|
||||||
|
<option value="year" {% if time_filter == 'year' %}selected{% endif %}>Last Year</option>
|
||||||
|
<option value="all" {% if time_filter == 'all' %}selected{% endif %}>All Time</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$('#time_filter').change(function () {
|
||||||
|
$(this).closest('form').submit();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h3>Overview</h3>
|
||||||
|
|
||||||
|
<!-- big cards in 2x2 grid with total_outstanding, total_events, total_invoices and total_payments, different backgrounds -->
|
||||||
|
|
||||||
|
<div class="card-deck">
|
||||||
|
<div class="card">
|
||||||
|
<a href="{% url 'invoice_waiting' %}" class="text-decoration-none text-white">
|
||||||
|
<div class="card-body bg-primary">
|
||||||
|
<h5 class="card-title text-center">Total Waiting</h5>
|
||||||
|
<p class="card-text text-center h3"><strong>£{{ total_waiting|floatformat:2 }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<a href="{% url 'invoice_list' %}" class="text-decoration-none text-dark">
|
||||||
|
<div class="card-body bg-info">
|
||||||
|
<h5 class="card-title text-center">Total Outstanding</h5>
|
||||||
|
<p class="card-text text-center h3"><strong>£{{ total_outstanding|floatformat:2 }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body bg-danger">
|
||||||
|
<h5 class="card-title text-center">Total Events</h5>
|
||||||
|
<p class="card-text text-center h3"><strong>{{ total_events }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body bg-success">
|
||||||
|
<h5 class="card-title text-center">Total Invoices</h5>
|
||||||
|
<p class="card-text text-center h3"><strong>{{ total_invoices }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<h3>Payments</h3>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<h4>Sources</h4>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{% for source in payment_methods %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><strong>{{ source.method }}</strong></h5>
|
||||||
|
<p class="card-text h3">£{{ source.total|floatformat:2 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<h4>Total</h4>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-center">Total Income</h5>
|
||||||
|
<p class="card-text text-center h3"><strong>£{{ total_income|floatformat:2 }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<h4>Invoice Payment Time</h4>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-center">Average Time to Pay</h5>
|
||||||
|
<p class="card-text text-center h3"><strong>{{ mean_invoice_to_payment|floatformat:2 }} days</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -115,7 +115,8 @@ urlpatterns = [
|
|||||||
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
|
path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'),
|
||||||
|
|
||||||
# Finance
|
# Finance
|
||||||
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceIndex.as_view()),
|
path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceDashboard.as_view()), name='invoice_dashboard'),
|
||||||
|
path('invoice/outstanding', permission_required_with_403('RIGS.view_invoice')(views.InvoiceOutstanding.as_view()),
|
||||||
name='invoice_list'),
|
name='invoice_list'),
|
||||||
path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceArchive.as_view()),
|
path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceArchive.as_view()),
|
||||||
name='invoice_archive'),
|
name='invoice_archive'),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import reversion
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Sum
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@@ -18,8 +18,76 @@ from RIGS import models
|
|||||||
|
|
||||||
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
forms.DateField.widget = forms.DateInput(attrs={'type': 'date'})
|
||||||
|
|
||||||
|
TIME_FILTERS = ["all", "year", "month", "week"]
|
||||||
|
|
||||||
class InvoiceIndex(generic.ListView):
|
|
||||||
|
def days_between(d1, d2):
|
||||||
|
diff = d2 - d1
|
||||||
|
return diff.total_seconds() / datetime.timedelta(days=1).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceDashboard(generic.TemplateView):
|
||||||
|
template_name = 'invoice_dashboard.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['page_title'] = "Invoice Dashboard"
|
||||||
|
context['description'] = "Overview of financial status of TEC rigs."
|
||||||
|
|
||||||
|
time_filter = self.request.GET.get('time_filter', 'all')
|
||||||
|
|
||||||
|
if time_filter not in TIME_FILTERS:
|
||||||
|
time_filter = 'all'
|
||||||
|
|
||||||
|
if time_filter == 'all':
|
||||||
|
context['events'] = models.Event.objects.all()
|
||||||
|
context['invoices'] = models.Invoice.objects.all()
|
||||||
|
context['payments'] = models.Payment.objects.all()
|
||||||
|
elif time_filter == 'year':
|
||||||
|
context['events'] = models.Event.objects.filter(start_date__gte=datetime.date.today() - datetime.timedelta(days=365))
|
||||||
|
context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=365))
|
||||||
|
context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=365))
|
||||||
|
elif time_filter == 'month':
|
||||||
|
context['events'] = models.Event.objects.filter(start_date__gte=datetime.date.today() - datetime.timedelta(days=30))
|
||||||
|
context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=30))
|
||||||
|
context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=30))
|
||||||
|
elif time_filter == 'week':
|
||||||
|
context['events'] = models.Event.objects.filter(start_date__gte=datetime.date.today() - datetime.timedelta(days=7))
|
||||||
|
context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=7))
|
||||||
|
context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=7))
|
||||||
|
|
||||||
|
context["time_filter"] = time_filter
|
||||||
|
|
||||||
|
context['total_outstanding'] = sum([i.balance for i in models.Invoice.objects.outstanding_invoices()])
|
||||||
|
context['total_waiting'] = sum([i.sum_total for i in models.Event.objects.waiting_invoices()])
|
||||||
|
context['total_events'] = len(context['events'])
|
||||||
|
context['total_invoices'] = len(context['invoices'])
|
||||||
|
context['total_payments'] = len(context['payments'])
|
||||||
|
|
||||||
|
payment_methods = dict(models.Payment.METHODS)
|
||||||
|
|
||||||
|
context['payment_methods'] = context["payments"].values('method').annotate(total=Sum('amount')).order_by('method')
|
||||||
|
|
||||||
|
for method in context['payment_methods']:
|
||||||
|
method['method'] = payment_methods.get(method['method'], f"Unknown method ({method['method']})")
|
||||||
|
|
||||||
|
context["total_income"] = sum([i['total'] for i in context['payment_methods']])
|
||||||
|
|
||||||
|
payments = context['payments']
|
||||||
|
mean_duration = 0
|
||||||
|
|
||||||
|
for payment in payments:
|
||||||
|
mean_duration += days_between(payment.invoice.invoice_date, payment.date)
|
||||||
|
|
||||||
|
if len(payments) > 0:
|
||||||
|
mean_duration /= len(payments)
|
||||||
|
|
||||||
|
context['mean_invoice_to_payment'] = mean_duration
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceOutstanding(generic.ListView):
|
||||||
model = models.Invoice
|
model = models.Invoice
|
||||||
template_name = 'invoice_list.html'
|
template_name = 'invoice_list.html'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user