Merge branch 'develop'

This commit is contained in:
David Taylor
2016-06-04 17:55:23 +01:00
8 changed files with 192 additions and 90 deletions

View File

@@ -1,32 +1,41 @@
import cStringIO as StringIO
import datetime
import re
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy
from django.db import connection
from django.http import Http404, HttpResponseRedirect
from django.views import generic
from django.template import RequestContext
from django.template.loader import get_template
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.contrib import messages
import datetime
from django.template import RequestContext
from django.template.loader import get_template
from django.views import generic
from django.db.models import Q
from z3c.rml import rml2pdf
from RIGS import models
import re
class InvoiceIndex(generic.ListView):
model = models.Invoice
template_name = 'RIGS/invoice_list.html'
template_name = 'RIGS/invoice_list_active.html'
def get_context_data(self, **kwargs):
context = super(InvoiceIndex, self).get_context_data(**kwargs)
total = 0
for i in context['object_list']:
total += i.balance
context['total'] = total
context['count'] = len(list(context['object_list']))
return context
def get_queryset(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \
"(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
@@ -40,6 +49,7 @@ class InvoiceIndex(generic.ListView):
class InvoiceDetail(generic.DetailView):
model = models.Invoice
class InvoicePrint(generic.View):
def get(self, request, pk):
invoice = get_object_or_404(models.Invoice, pk=pk)
@@ -54,8 +64,8 @@ class InvoicePrint(generic.View):
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
}
},
'invoice':invoice,
'current_user':request.user,
'invoice': invoice,
'current_user': request.user,
})
rml = template.render(context)
@@ -72,6 +82,7 @@ class InvoicePrint(generic.View):
response.write(pdfData)
return response
class InvoiceVoid(generic.View):
def get(self, *args, **kwargs):
pk = kwargs.get('pk')
@@ -86,6 +97,7 @@ class InvoiceVoid(generic.View):
class InvoiceArchive(generic.ListView):
model = models.Invoice
template_name = 'RIGS/invoice_list_archive.html'
paginate_by = 25
@@ -94,14 +106,31 @@ class InvoiceWaiting(generic.ListView):
paginate_by = 25
template_name = 'RIGS/event_invoice.html'
def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
total = 0
for obj in self.get_objects():
total += obj.sum_total
context['total'] = total
return context
def get_queryset(self):
return self.get_objects()
def get_objects(self):
# @todo find a way to select items
events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(),
invoice__isnull=True) \
.order_by('start_date') \
events = self.model.objects.filter(
(
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
) & Q(invoice__isnull=True) # Has not already been invoiced
).order_by('start_date') \
.select_related('person',
'organisation',
'venue', 'mic')
'venue', 'mic') \
.prefetch_related('items')
return events
@@ -119,7 +148,7 @@ class InvoiceEvent(generic.View):
class PaymentCreate(generic.CreateView):
model = models.Payment
fields = ['invoice','date','amount','method']
fields = ['invoice', 'date', 'amount', 'method']
def get_initial(self):
initial = super(generic.CreateView, self).get_initial()
@@ -139,4 +168,4 @@ class PaymentDelete(generic.DeleteView):
model = models.Payment
def get_success_url(self):
return self.request.POST.get('next')
return self.request.POST.get('next')

View File

@@ -1,31 +1,32 @@
import datetime
import hashlib
import datetime, pytz
from django.db import models, connection
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
import reversion
import string
import pytz
import random
import string
from collections import Counter
from django.core.urlresolvers import reverse_lazy
from django.core.exceptions import ValidationError
from decimal import Decimal
import reversion
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse_lazy
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
# Create your models here.
@python_2_unicode_compatible
class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True)
api_key = models.CharField(max_length=40,blank=True,editable=False, null=True)
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
@classmethod
def make_api_key(cls):
size=20
chars=string.ascii_letters + string.digits
size = 20
chars = string.ascii_letters + string.digits
new_api_key = ''.join(random.choice(chars) for x in range(size))
return new_api_key;
@@ -55,6 +56,7 @@ class Profile(AbstractUser):
('view_profile', 'Can view Profile'),
)
class RevisionMixin(object):
@property
def last_edited_at(self):
@@ -79,10 +81,11 @@ class RevisionMixin(object):
versions = reversion.get_for_object(self)
if versions:
version = reversion.get_for_object(self)[0]
return "V{0} | R{1}".format(version.pk,version.revision.pk)
return "V{0} | R{1}".format(version.pk, version.revision.pk)
else:
return None
@reversion.register
@python_2_unicode_compatible
class Person(models.Model, RevisionMixin):
@@ -97,7 +100,7 @@ class Person(models.Model, RevisionMixin):
def __str__(self):
string = self.name
if self.notes is not None:
if len(self.notes) > 0:
if len(self.notes) > 0:
string += "*"
return string
@@ -108,7 +111,7 @@ class Person(models.Model, RevisionMixin):
if e.organisation:
o.append(e.organisation)
#Count up occurances and put them in descending order
# Count up occurances and put them in descending order
c = Counter(o)
stats = c.most_common()
return stats
@@ -141,7 +144,7 @@ class Organisation(models.Model, RevisionMixin):
def __str__(self):
string = self.name
if self.notes is not None:
if len(self.notes) > 0:
if len(self.notes) > 0:
string += "*"
return string
@@ -151,8 +154,8 @@ class Organisation(models.Model, RevisionMixin):
for e in Event.objects.filter(organisation=self).select_related('person'):
if e.person:
p.append(e.person)
#Count up occurances and put them in descending order
# Count up occurances and put them in descending order
c = Counter(p)
stats = c.most_common()
return stats
@@ -238,12 +241,18 @@ class Venue(models.Model, RevisionMixin):
class EventManager(models.Manager):
def current_events(self):
events = self.filter(
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(
status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
return events
def events_in_bounds(self, start, end):
@@ -251,15 +260,17 @@ class EventManager(models.Manager):
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
return events
def rig_count(self):
@@ -301,7 +312,8 @@ class Event(models.Model, RevisionMixin):
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True)
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True, null=True)
based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
null=True)
# Timing
start_date = models.DateField()
@@ -327,6 +339,7 @@ class Event(models.Model, RevisionMixin):
"""
EX Vat
"""
@property
def sum_total(self):
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
@@ -334,14 +347,15 @@ class Event(models.Model, RevisionMixin):
# sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
# else:
# sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
#total = self.items.raw(sql)[0]
#if total.sum_total:
# total = self.items.raw(sql)[0]
# if total.sum_total:
# return total.sum_total
#total = 0.0
#for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
# total = 0.0
# for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
# total += item.sum
total = EventItem.objects.filter(event=self).aggregate(
sum_total=models.Sum(models.F('cost')*models.F('quantity'), output_field=models.DecimalField(max_digits=10, decimal_places=2))
sum_total=models.Sum(models.F('cost') * models.F('quantity'),
output_field=models.DecimalField(max_digits=10, decimal_places=2))
)['sum_total']
if total:
return total
@@ -358,6 +372,7 @@ class Event(models.Model, RevisionMixin):
"""
Inc VAT
"""
@property
def total(self):
return self.sum_total + self.vat
@@ -382,7 +397,7 @@ class Event(models.Model, RevisionMixin):
def earliest_time(self):
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
#Put all the datetimes in a list
# Put all the datetimes in a list
datetime_list = []
if self.access_at:
@@ -394,22 +409,22 @@ class Event(models.Model, RevisionMixin):
# If there is no start time defined, pretend it's midnight
startTimeFaked = False
if self.has_start_time:
startDateTime = datetime.datetime.combine(self.start_date,self.start_time)
startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
else:
startDateTime = datetime.datetime.combine(self.start_date,datetime.time(00,00))
startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
startTimeFaked = True
#timezoneIssues - apply the default timezone to the naiive datetime
# timezoneIssues - apply the default timezone to the naiive datetime
tz = pytz.timezone(settings.TIME_ZONE)
startDateTime = tz.localize(startDateTime)
datetime_list.append(startDateTime) # then add it to the list
datetime_list.append(startDateTime) # then add it to the list
earliest = min(datetime_list).astimezone(tz) #find the earliest datetime in the list
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
# if we faked it & it's the earliest, better own up
if startTimeFaked and earliest==startDateTime:
if startTimeFaked and earliest == startDateTime:
return self.start_date
return earliest
@property
@@ -421,7 +436,7 @@ class Event(models.Model, RevisionMixin):
endDate = self.start_date
if self.has_end_time:
endDateTime = datetime.datetime.combine(endDate,self.end_time)
endDateTime = datetime.datetime.combine(endDate, self.end_time)
tz = pytz.timezone(settings.TIME_ZONE)
endDateTime = tz.localize(endDateTime)
@@ -430,7 +445,6 @@ class Event(models.Model, RevisionMixin):
else:
return endDate
objects = EventManager()
def get_absolute_url(self):
@@ -446,7 +460,7 @@ class Event(models.Model, RevisionMixin):
startEndSameDay = not self.end_date or self.end_date == self.start_date
hasStartAndEnd = self.has_start_time and self.has_end_time
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
def save(self, *args, **kwargs):
"""Call :meth:`full_clean` before saving."""
@@ -503,15 +517,6 @@ class Invoice(models.Model):
@property
def payment_total(self):
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
#if connection.vendor == 'postgresql':
# sql = "SELECT SUM(amount) AS total FROM \"RIGS_payment\" WHERE invoice_id=%i" % self.id
#else:
# sql = "SELECT id, SUM(amount) AS total FROM RIGS_payment WHERE invoice_id=%i" % self.id
#total = self.payment_set.raw(sql)[0]
#if total.total:
# return total.total
#return 0.0
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
if total:
return total
@@ -552,4 +557,4 @@ class Payment(models.Model):
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
def __str__(self):
return "%s: %d" % (self.get_method_display(), self.amount)
return "%s: %d" % (self.get_method_display(), self.amount)

View File

@@ -1,11 +1,22 @@
{% extends 'base.html' %}
{% load paginator from filters %}
{% load static %}
{% block title %}Events for Invoice{% endblock %}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %}
<div class="col-sm-12">
<h2>Events for Invoice</h2>
<h2>Events for Invoice ({{object_list|length}} Events, £ {{ total|floatformat:2 }})</h2>
<p>These events have happened, but paperwork has not yet been sent to treasury</p>
{% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %}
@@ -15,8 +26,8 @@
<thead>
<tr>
<th class="hiddenx-xs">#</th>
<th>Date</th>
<th>Event</th>
<th>Start Date</th>
<th>Event Name</th>
<th>Client</th>
<th>Cost</th>
<th class="hidden-xs">MIC</th>
@@ -39,23 +50,33 @@
danger
{% endif %}
">
<td class="hidden-xs"><a href="{% url 'event_detail' object.pk %}" target="_blank">N{{ object.pk|stringformat:"05d" }}</a></td>
<td>{{ object.end_date }}</td>
<td class="hidden-xs"><a href="{% url 'event_detail' object.pk %}">N{{ object.pk|stringformat:"05d" }}</a><br>
<span class="text-muted">{{ object.get_status_display }}</span></td>
<td>{{ object.start_date }}</td>
<td>{{ object.name }}</td>
<td>
{% if object.organisation %}
{{ object.organisation.name }}
<br>
<span class="text-muted">{{ object.organisation.union_account|yesno:'Internal,External' }}</span>
{% else %}
{{ object.person.name }}
<br>
<span class="text-muted">External</span>
{% endif %}
</td>
<td>{{ object.sum_total|floatformat:2 }}</td>
<td class="text-center">
{{ object.mic.initials }}<br/>
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
{% if object.mic %}
{{ object.mic.initials }}<br>
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
{% else %}
<span class="glyphicon glyphicon-exclamation-sign"></span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'invoice_event' object.pk %}" target="_blank" class="btn btn-default">
<a href="{% url 'invoice_event' object.pk %}" class="btn btn-default" data-toggle="tooltip" title="'Invoice' this event - click this when paperwork has been sent to treasury">
<span class="glyphicon glyphicon-gbp"></span>
</a>
</td>

View File

@@ -109,6 +109,11 @@
</td>
</tr>
{% endfor %}
<tr>
<td class="text-right"><strong>Balance:</strong></td>
<td>{{ object.balance|floatformat:2 }}</td>
<td></td>
<td></td>
</tbody>
</table>
</div>

View File

@@ -5,7 +5,8 @@
{% block content %}
<div class="col-sm-12">
<h2>Invoices</h2>
<h2>{% block heading %}Invoices{% endblock %}</h2>
{% block description %}{% endblock %}
{% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %}
@@ -16,6 +17,8 @@
<tr>
<th>#</th>
<th>Event</th>
<th>Client</th>
<th>Event Date</th>
<th>Invoice Date</th>
<th>Balance</th>
<th></th>
@@ -25,7 +28,20 @@
{% for object in object_list %}
<tr class="{% if object.void %}danger{% elif object.balance == 0 %}success{% endif %}">
<td>{{ object.pk }}</td>
<td><a href="{% url 'event_detail' object.event.pk %}" target="_blank">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }}</td>
<td><a href="{% url 'event_detail' object.event.pk %}">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }} <br>
<span class="text-muted">{{ object.event.get_status_display }}</span></td>
</td>
<td>{% if object.organisation %}
{{ object.event.organisation.name }}
<br>
<span class="text-muted">{{ object.organisation.union_account|yesno:'Internal,External' }}</span>
{% else %}
{{ object.event.person.name }}
<br>
<span class="text-muted">External</span>
{% endif %}
</td>
<td>{{ object.event.start_date }}</td>
<td>{{ object.invoice_date }}</td>
<td>{{ object.balance|floatformat:2 }}</td>
<td class="text-right">

View File

@@ -0,0 +1,13 @@
{% extends 'RIGS/invoice_list.html' %}
{% block title %}
Outstanding Invoices
{% endblock %}
{% block heading %}
Outstanding Invoices ({{ count }} Events, £ {{ total|floatformat:2 }})
{% endblock %}
{% block description %}
<p>Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger</p>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'RIGS/invoice_list.html' %}
{% block title %}
Invoice Archive
{% endblock %}
{% block heading %}
All Invoices
{% endblock %}
{% block description %}
<p>This page displays all invoices: outstanding, paid, and void</p>
{% endblock %}

View File

@@ -74,12 +74,12 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Active</a>
</li>
{% if perms.RIGS.add_invoice %}
<li><a href="{% url 'invoice_waiting' %}"><span
class="glyphicon glyphicon-briefcase"></span> Waiting</a></li>
{% endif %}
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Outstanding</a>
</li>
<li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span>
Archive</a></li>
</ul>