nk20/apps/treasury/views.py

457 lines
16 KiB
Python

# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import shutil
import subprocess
from tempfile import mkdtemp
from crispy_forms.helper import FormHelper
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError, PermissionDenied
from django.db import transaction
from django.db.models import Q
from django.forms import Form
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView, DetailView
from django.views.generic.base import View, TemplateView
from django.views.generic.edit import BaseFormView, DeleteView
from django_tables2 import SingleTableView
from note.models import SpecialTransaction, NoteSpecial, Alias
from note_kfet.settings.base import BASE_DIR
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable
class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Create Invoice
"""
model = Invoice
form_class = InvoiceForm
extra_context = {"title": _("Create new invoice")}
def get_sample_object(self):
return Invoice(
id=0,
object="",
description="",
name="",
address="",
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = context['form']
form.helper = FormHelper()
# Remove form tag on the generation of the form in the template (already present on the template)
form.helper.form_tag = False
# The formset handles the set of the products
form_set = ProductFormSet(instance=form.instance)
context['formset'] = form_set
context['helper'] = ProductFormSetHelper()
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
del form.fields["locked"]
return form
@transaction.atomic
def form_valid(self, form):
ret = super().form_valid(form)
# For each product, we save it
formset = ProductFormSet(self.request.POST, instance=form.instance)
if formset.is_valid():
for f in formset:
# We don't save the product if the designation is not entered, ie. if the line is empty
if f.is_valid() and f.instance.designation:
f.save()
f.instance.save()
else:
f.instance = None
return ret
def get_success_url(self):
return reverse_lazy('treasury:invoice_list')
class InvoiceListView(LoginRequiredMixin, SingleTableView):
"""
List existing Invoices
"""
model = Invoice
table_class = InvoiceTable
extra_context = {"title": _("Invoices list")}
def dispatch(self, request, *args, **kwargs):
# Check that the user is authenticated
if not request.user.is_authenticated:
return self.handle_no_permission()
sample_invoice = Invoice(
id=0,
object="",
description="",
name="",
address="",
)
if not PermissionBackend.check_perm(self.request.user, "treasury.add_invoice", sample_invoice):
raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs)
class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Create Invoice
"""
model = Invoice
form_class = InvoiceForm
extra_context = {"title": _("Update an invoice")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = context['form']
form.helper = FormHelper()
# Remove form tag on the generation of the form in the template (already present on the template)
form.helper.form_tag = False
# The formset handles the set of the products
form_set = ProductFormSet(instance=self.object)
context['formset'] = form_set
context['helper'] = ProductFormSetHelper()
if self.object.locked:
for field_name in form.fields:
form.fields[field_name].disabled = True
for f in form_set.forms:
for field_name in f.fields:
f.fields[field_name].disabled = True
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
del form.fields["id"]
return form
@transaction.atomic
def form_valid(self, form):
ret = super().form_valid(form)
formset = ProductFormSet(self.request.POST, instance=form.instance)
saved = []
# For each product, we save it
if formset.is_valid():
for f in formset:
# We don't save the product if the designation is not entered, ie. if the line is empty
if f.is_valid() and f.instance.designation:
f.save()
f.instance.save()
saved.append(f.instance.pk)
else:
f.instance = None
# Remove old products that weren't given in the form
Product.objects.filter(~Q(pk__in=saved), invoice=form.instance).delete()
return ret
def get_success_url(self):
return reverse_lazy('treasury:invoice_list')
class InvoiceDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
"""
Delete a non-validated WEI registration
"""
model = Invoice
extra_context = {"title": _("Delete invoice")}
def delete(self, request, *args, **kwargs):
if self.get_object().locked:
raise PermissionDenied(_("This invoice is locked and can't be deleted."))
return super().delete(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy('treasury:invoice_list')
class InvoiceRenderView(LoginRequiredMixin, View):
"""
Render Invoice as a generated PDF with the given information and a LaTeX template
"""
def get(self, request, **kwargs):
pk = kwargs["pk"]
invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk)
tex = invoice.tex
try:
os.mkdir(BASE_DIR + "/tmp")
except FileExistsError:
pass
# We render the file in a temporary directory
tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/")
try:
with open("{}/invoice-{:d}.tex".format(tmp_dir, pk), "wb") as f:
f.write(tex.encode("UTF-8"))
del tex
# The file has to be rendered twice
for ignored in range(2):
error = subprocess.Popen(
["/usr/bin/xelatex", "-interaction=nonstopmode", "invoice-{}.tex".format(pk)],
cwd=tmp_dir,
stdin=open(os.devnull, "r"),
stderr=open(os.devnull, "wb"),
stdout=open(os.devnull, "wb"),
).wait()
if error:
with open("{}/invoice-{:d}.log".format(tmp_dir, pk), "r") as f:
log = f.read()
raise IOError("An error attempted while generating a invoice (code=" + str(error) + ")\n\n" + log)
# Display the generated pdf as a HTTP Response
pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()
response = HttpResponse(pdf, content_type="application/pdf")
response['Content-Disposition'] = "inline;filename=Facture%20n°{:d}.pdf".format(pk)
except IOError as e:
raise e
finally:
# Delete all temporary files
shutil.rmtree(tmp_dir)
return response
class RemittanceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
Create Remittance
"""
model = Remittance
form_class = RemittanceForm
extra_context = {"title": _("Create a new remittance")}
def get_sample_object(self):
return Remittance(
remittance_type_id=1,
comment="",
)
def get_success_url(self):
return reverse_lazy('treasury:remittance_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["table"] = RemittanceTable(
data=Remittance.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
return context
class RemittanceListView(LoginRequiredMixin, TemplateView):
"""
List existing Remittances
"""
template_name = "treasury/remittance_list.html"
extra_context = {"title": _("Remittances list")}
def dispatch(self, request, *args, **kwargs):
# Check that the user is authenticated
if not request.user.is_authenticated:
return self.handle_no_permission()
sample_remittance = Remittance(
remittance_type_id=1,
comment="",
)
if not PermissionBackend.check_perm(self.request.user, "treasury.add_remittance", sample_remittance):
raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
opened_remittances = RemittanceTable(
data=Remittance.objects.filter(closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
prefix="opened-remittances-",
)
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
context["opened_remittances"] = opened_remittances
closed_remittances = RemittanceTable(
data=Remittance.objects.filter(closed=True).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
prefix="closed-remittances-",
)
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
context["closed_remittances"] = closed_remittances
no_remittance_tr = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance=None).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
exclude=('remittance_remove', ),
prefix="no-remittance-",
)
no_remittance_tr.paginate(page=self.request.GET.get("no-remittance-page", 1), per_page=10)
context["special_transactions_no_remittance"] = no_remittance_tr
with_remittance_tr = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance__closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
exclude=('remittance_add', ),
prefix="with-remittance-",
)
with_remittance_tr.paginate(page=self.request.GET.get("with-remittance-page", 1), per_page=10)
context["special_transactions_with_remittance"] = with_remittance_tr
return context
class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update Remittance
"""
model = Remittance
form_class = RemittanceForm
extra_context = {"title": _("Update a remittance")}
def get_success_url(self):
return reverse_lazy('treasury:remittance_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()
context["special_transactions"] = SpecialTransactionTable(
data=data,
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
return context
class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Attach a special transaction to a remittance
"""
model = SpecialTransactionProxy
form_class = LinkTransactionToRemittanceForm
extra_context = {"title": _("Attach a transaction to a remittance")}
def get_success_url(self):
return reverse_lazy('treasury:remittance_list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = context["form"]
form.fields["last_name"].initial = self.object.transaction.last_name
form.fields["first_name"].initial = self.object.transaction.first_name
form.fields["bank"].initial = self.object.transaction.bank
form.fields["amount"].initial = self.object.transaction.amount
form.fields["remittance"].queryset = form.fields["remittance"] \
.queryset.filter(remittance_type__note=self.object.transaction.source)
return context
class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View):
"""
Unlink a special transaction and its remittance
"""
def get(self, *args, **kwargs):
pk = kwargs["pk"]
transaction = SpecialTransactionProxy.objects.get(pk=pk)
# The remittance must be open (or inexistant)
if transaction.remittance and transaction.remittance.closed:
raise ValidationError("Remittance is already closed.")
transaction.remittance = None
transaction.save()
return redirect('treasury:remittance_list')
class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView):
"""
List all Société Générale credits
"""
model = SogeCredit
table_class = SogeCreditTable
extra_context = {"title": _("List of credits from the Société générale")}
def dispatch(self, request, *args, **kwargs):
# Check that the user is authenticated
if not request.user.is_authenticated:
return self.handle_no_permission()
if not super().get_queryset().exists():
raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs)
def get_queryset(self, **kwargs):
"""
Filter the table with the given parameter.
:param kwargs:
:return:
"""
qs = super().get_queryset()
if "search" in self.request.GET:
pattern = self.request.GET["search"]
if pattern:
qs = qs.filter(
Q(user__first_name__iregex=pattern)
| Q(user__last_name__iregex=pattern)
| Q(user__note__alias__name__iregex="^" + pattern)
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
)
if "valid" not in self.request.GET or not self.request.GET["valid"]:
qs = qs.filter(credit_transaction__valid=False)
return qs
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):
"""
Manage credits from the Société générale.
"""
model = SogeCredit
form_class = Form
extra_context = {"title": _("Manage credits from the Société générale")}
@transaction.atomic
def form_valid(self, form):
if "validate" in form.data:
self.get_object().validate(True)
elif "delete" in form.data:
self.get_object().delete()
return super().form_valid(form)
def get_success_url(self):
if "validate" in self.request.POST:
return reverse_lazy('treasury:manage_soge_credit', args=(self.get_object().pk,))
return reverse_lazy('treasury:soge_credits')