# Copyright (C) 2018-2020 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 from django.db.models import Q from django.forms import Form from django.http import HttpResponse from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, UpdateView, DetailView from django.views.generic.base import View, TemplateView from django.views.generic.edit import BaseFormView 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 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, LoginRequiredMixin, CreateView): """ Create Invoice """ model = Invoice form_class = InvoiceForm extra_context = {"title": _("Create new 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=form.instance) context['formset'] = form_set context['helper'] = ProductFormSetHelper() context['no_cache'] = True return context 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(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List existing Invoices """ model = Invoice table_class = InvoiceTable extra_context = {"title": _("Invoices list")} 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 # Fill the intial value for the date field, with the initial date of the model instance form.fields['date'].initial = form.instance.date # The formset handles the set of the products form_set = ProductFormSet(instance=form.instance) context['formset'] = form_set context['helper'] = ProductFormSetHelper() context['no_cache'] = True return context 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 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) products = Product.objects.filter(invoice=invoice).all() # Informations of the BDE. Should be updated when the school will move. invoice.place = "Cachan" invoice.my_name = "BDE ENS Cachan" invoice.my_address_street = "61 avenue du Président Wilson" invoice.my_city = "94230 Cachan" invoice.bank_code = 30003 invoice.desk_code = 3894 invoice.account_number = 37280662 invoice.rib_key = 14 invoice.bic = "SOGEFRPP" # Replace line breaks with the LaTeX equivalent invoice.description = invoice.description.replace("\r", "").replace("\n", "\\\\ ") invoice.address = invoice.address.replace("\r", "").replace("\n", "\\\\ ") # Fill the template with the information tex = render_to_string("treasury/invoice_sample.tex", dict(obj=invoice, products=products)) 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 _ in range(2): error = subprocess.Popen( ["pdflatex", "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: raise IOError("An error attempted while generating a invoice (code=" + str(error) + ")") # 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, LoginRequiredMixin, CreateView): """ Create Remittance """ model = Remittance form_class = RemittanceForm extra_context = {"title": _("Create a new remittance")} 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 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")).reverse().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) context["table"] = RemittanceTable(data=Remittance.objects.filter( PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) 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 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 not pattern: return qs.none() 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)) ) else: qs = qs.none() if "valid" in self.request.GET: q = Q(credit_transaction=None) if not self.request.GET["valid"]: q = ~q qs = qs.filter(q) return qs[:20] 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")} 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')