bornhack-website/src/backoffice/views/economy.py

444 lines
15 KiB
Python

import logging
import os
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.files import File
from django.db.models import Count, Q, Sum
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.views.generic import DetailView, ListView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from camps.mixins import CampViewMixin
from economy.models import Chain, Credebtor, Expense, Reimbursement, Revenue
from teams.models import Team
from ..mixins import EconomyTeamPermissionMixin
logger = logging.getLogger("bornhack.%s" % __name__)
################################
# CHAINS & CREDEBTORS
class ChainListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Chain
template_name = "chain_list_backoffice.html"
def get_queryset(self, *args, **kwargs):
"""Annotate the total count and amount for expenses and revenues for all credebtors in each chain."""
qs = Chain.objects.annotate(
camp_expenses_amount=Sum(
"credebtors__expenses__amount",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_expenses_count=Count(
"credebtors__expenses",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_revenues_amount=Sum(
"credebtors__revenues__amount",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
camp_revenues_count=Count(
"credebtors__revenues",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
)
return qs
class ChainDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Chain
template_name = "chain_detail_backoffice.html"
slug_url_kwarg = "chain_slug"
def get_queryset(self, *args, **kwargs):
"""Annotate the Chain object with the camp filtered expense and revenue info."""
qs = super().get_queryset(*args, **kwargs)
qs = qs.annotate(
camp_expenses_amount=Sum(
"credebtors__expenses__amount",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_expenses_count=Count(
"credebtors__expenses",
filter=Q(credebtors__expenses__camp=self.camp),
distinct=True,
),
camp_revenues_amount=Sum(
"credebtors__revenues__amount",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
camp_revenues_count=Count(
"credebtors__revenues",
filter=Q(credebtors__revenues__camp=self.camp),
distinct=True,
),
)
return qs
def get_context_data(self, *args, **kwargs):
"""Add credebtors, expenses and revenues to the context in camp-filtered versions."""
context = super().get_context_data(*args, **kwargs)
# include credebtors as a seperate queryset with annotations for total number and
# amount of expenses and revenues
context["credebtors"] = Credebtor.objects.filter(
chain=self.get_object()
).annotate(
camp_expenses_amount=Sum(
"expenses__amount", filter=Q(expenses__camp=self.camp), distinct=True
),
camp_expenses_count=Count(
"expenses", filter=Q(expenses__camp=self.camp), distinct=True
),
camp_revenues_amount=Sum(
"revenues__amount", filter=Q(revenues__camp=self.camp), distinct=True
),
camp_revenues_count=Count(
"revenues", filter=Q(revenues__camp=self.camp), distinct=True
),
)
# Include expenses and revenues for the Chain in context as seperate querysets,
# since accessing them through the relatedmanager returns for all camps
context["expenses"] = Expense.objects.filter(
camp=self.camp, creditor__chain=self.get_object()
).prefetch_related("responsible_team", "user", "creditor")
context["revenues"] = Revenue.objects.filter(
camp=self.camp, debtor__chain=self.get_object()
).prefetch_related("responsible_team", "user", "debtor")
return context
class CredebtorDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Credebtor
template_name = "credebtor_detail_backoffice.html"
slug_url_kwarg = "credebtor_slug"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context["expenses"] = (
self.get_object()
.expenses.filter(camp=self.camp)
.prefetch_related("responsible_team", "user", "creditor")
)
context["revenues"] = (
self.get_object()
.revenues.filter(camp=self.camp)
.prefetch_related("responsible_team", "user", "debtor")
)
return context
################################
# EXPENSES
class ExpenseListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Expense
template_name = "expense_list_backoffice.html"
def get_queryset(self, **kwargs):
"""
Exclude unapproved expenses, they are shown seperately
"""
queryset = super().get_queryset(**kwargs)
return queryset.exclude(approved__isnull=True).prefetch_related(
"creditor",
"user",
"responsible_team",
)
def get_context_data(self, **kwargs):
"""
Include unapproved expenses seperately
"""
context = super().get_context_data(**kwargs)
context["unapproved_expenses"] = Expense.objects.filter(
camp=self.camp, approved__isnull=True
).prefetch_related(
"creditor",
"user",
"responsible_team",
)
return context
class ExpenseDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
model = Expense
template_name = "expense_detail_backoffice.html"
fields = ["notes"]
def form_valid(self, form):
"""
We have two submit buttons in this form, Approve and Reject
"""
expense = form.save()
if "approve" in form.data:
# approve button was pressed
expense.approve(self.request)
elif "reject" in form.data:
# reject button was pressed
expense.reject(self.request)
else:
messages.error(self.request, "Unknown submit action")
return redirect(
reverse("backoffice:expense_list", kwargs={"camp_slug": self.camp.slug})
)
######################################
# REIMBURSEMENTS
class ReimbursementListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Reimbursement
template_name = "reimbursement_list_backoffice.html"
class ReimbursementDetailView(CampViewMixin, EconomyTeamPermissionMixin, DetailView):
model = Reimbursement
template_name = "reimbursement_detail_backoffice.html"
class ReimbursementCreateUserSelectView(
CampViewMixin, EconomyTeamPermissionMixin, ListView
):
template_name = "reimbursement_create_userselect.html"
def get_queryset(self):
queryset = User.objects.filter(
id__in=Expense.objects.filter(
camp=self.camp,
reimbursement__isnull=True,
paid_by_bornhack=False,
approved=True,
)
.values_list("user", flat=True)
.distinct()
)
return queryset
class ReimbursementCreateView(CampViewMixin, EconomyTeamPermissionMixin, CreateView):
model = Reimbursement
template_name = "reimbursement_create.html"
fields = ["notes", "paid"]
def dispatch(self, request, *args, **kwargs):
"""Get the user from kwargs"""
self.reimbursement_user = get_object_or_404(User, pk=kwargs["user_id"])
# get response now so we have self.camp available below
response = super().dispatch(request, *args, **kwargs)
# return the response
return response
def get(self, request, *args, **kwargs):
# does this user have any approved and un-reimbursed expenses?
if not self.reimbursement_user.expenses.filter(
reimbursement__isnull=True, approved=True, paid_by_bornhack=False
):
messages.error(
request, "This user has no approved and unreimbursed expenses!"
)
return redirect(
reverse("backoffice:index", kwargs={"camp_slug": self.camp.slug})
)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["expenses"] = Expense.objects.filter(
user=self.reimbursement_user,
approved=True,
reimbursement__isnull=True,
paid_by_bornhack=False,
)
context["total_amount"] = context["expenses"].aggregate(Sum("amount"))
context["reimbursement_user"] = self.reimbursement_user
return context
def form_valid(self, form):
"""
Set user and camp for the Reimbursement before saving
"""
# get the expenses for this user
expenses = Expense.objects.filter(
user=self.reimbursement_user,
approved=True,
reimbursement__isnull=True,
paid_by_bornhack=False,
)
if not expenses:
messages.error(self.request, "No expenses found")
return redirect(
reverse(
"backoffice:reimbursement_list",
kwargs={"camp_slug": self.camp.slug},
)
)
# get the Economy team for this camp
try:
economyteam = Team.objects.get(
camp=self.camp, name=settings.ECONOMYTEAM_NAME
)
except Team.DoesNotExist:
messages.error(self.request, "No economy team found")
return redirect(
reverse(
"backoffice:reimbursement_list",
kwargs={"camp_slug": self.camp.slug},
)
)
# create reimbursement in database
reimbursement = form.save(commit=False)
reimbursement.reimbursement_user = self.reimbursement_user
reimbursement.user = self.request.user
reimbursement.camp = self.camp
reimbursement.save()
# add all expenses to reimbursement
for expense in expenses:
expense.reimbursement = reimbursement
expense.save()
# create expense for this reimbursement
expense = Expense()
expense.camp = self.camp
expense.user = self.request.user
expense.amount = reimbursement.amount
expense.description = "Payment of reimbursement %s to %s" % (
reimbursement.pk,
reimbursement.reimbursement_user,
)
expense.paid_by_bornhack = True
expense.responsible_team = economyteam
expense.approved = True
expense.reimbursement = reimbursement
expense.invoice_date = timezone.now()
expense.creditor = Credebtor.objects.get(name="Reimbursement")
expense.invoice.save(
"na.jpg",
File(
open(
os.path.join(settings.DJANGO_BASE_PATH, "static_src/img/na.jpg"),
"rb",
)
),
)
expense.save()
messages.success(
self.request,
"Reimbursement %s has been created with invoice_date %s"
% (reimbursement.pk, timezone.now()),
)
return redirect(
reverse(
"backoffice:reimbursement_detail",
kwargs={"camp_slug": self.camp.slug, "pk": reimbursement.pk},
)
)
class ReimbursementUpdateView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
model = Reimbursement
template_name = "reimbursement_form.html"
fields = ["notes", "paid"]
def get_success_url(self):
return reverse(
"backoffice:reimbursement_detail",
kwargs={"camp_slug": self.camp.slug, "pk": self.get_object().pk},
)
class ReimbursementDeleteView(CampViewMixin, EconomyTeamPermissionMixin, DeleteView):
model = Reimbursement
template_name = "reimbursement_delete.html"
fields = ["notes", "paid"]
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if self.get_object().paid:
messages.error(
request,
"This reimbursement has already been paid so it cannot be deleted",
)
return redirect(
reverse(
"backoffice:reimbursement_list",
kwargs={"camp_slug": self.camp.slug},
)
)
return response
################################
# REVENUES
class RevenueListView(CampViewMixin, EconomyTeamPermissionMixin, ListView):
model = Revenue
template_name = "revenue_list_backoffice.html"
def get_queryset(self, **kwargs):
"""
Exclude unapproved revenues, they are shown seperately
"""
queryset = super().get_queryset(**kwargs)
return queryset.exclude(approved__isnull=True).prefetch_related(
"debtor",
"user",
"responsible_team",
)
def get_context_data(self, **kwargs):
"""
Include unapproved revenues seperately
"""
context = super().get_context_data(**kwargs)
context["unapproved_revenues"] = Revenue.objects.filter(
camp=self.camp, approved__isnull=True
)
return context
class RevenueDetailView(CampViewMixin, EconomyTeamPermissionMixin, UpdateView):
model = Revenue
template_name = "revenue_detail_backoffice.html"
fields = ["notes"]
def form_valid(self, form):
"""
We have two submit buttons in this form, Approve and Reject
"""
revenue = form.save()
if "approve" in form.data:
# approve button was pressed
revenue.approve(self.request)
elif "reject" in form.data:
# reject button was pressed
revenue.reject(self.request)
else:
messages.error(self.request, "Unknown submit action")
return redirect(
reverse("backoffice:revenue_list", kwargs={"camp_slug": self.camp.slug})
)