# Copyright (C) 2018-2024 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 MultiTableMixin, SingleTableMixin, 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, SogeCreditForm 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() if not PermissionBackend.has_model_perm(self.request, Invoice(), "view"): 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, 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, Remittance, "view")).all()) context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) return context class RemittanceListView(LoginRequiredMixin, MultiTableMixin, TemplateView): """ List existing Remittances """ template_name = "treasury/remittance_list.html" extra_context = {"title": _("Remittances list")} tables = [ lambda data: RemittanceTable(data, prefix="opened-remittances-"), lambda data: RemittanceTable(data, prefix="closed-remittances-"), lambda data: SpecialTransactionTable(data, prefix="no-remittance-", exclude=('remittance_remove', )), lambda data: SpecialTransactionTable(data, prefix="with-remittance-", exclude=('remittance_add', )), ] paginate_by = 10 # number of rows in tables 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 PermissionBackend.has_model_perm(self.request, Remittance(), "view"): raise PermissionDenied(_("You are not able to see the treasury interface.")) return super().dispatch(request, *args, **kwargs) def get_tables_data(self): return [ Remittance.objects.filter(closed=False).filter( PermissionBackend.filter_queryset(self.request, Remittance, "view")), Remittance.objects.filter(closed=True).filter( PermissionBackend.filter_queryset(self.request, Remittance, "view")), SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), specialtransactionproxy__remittance=None).filter( PermissionBackend.filter_queryset(self.request, Remittance, "view")), SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), specialtransactionproxy__remittance__closed=False).filter( PermissionBackend.filter_queryset(self.request, Remittance, "view")), ] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tables = context["tables"] names = [ "opened_remittances", "closed_remittances", "special_transactions_no_remittance", "special_transactions_with_remittance", ] for name, table in zip(names, tables): context[name] = table return context class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, UpdateView): """ Update Remittance """ model = Remittance form_class = RemittanceForm extra_context = {"title": _("Update a remittance")} table_class = SpecialTransactionTable context_table_name = "special_transactions" def get_success_url(self): return reverse_lazy('treasury:remittance_list') def get_table_data(self): return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter( PermissionBackend.filter_queryset(self.request, Remittance, "view")) def get_table_kwargs(self): return {"exclude": ('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )} 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 PermissionBackend.has_model_perm(self.request, SogeCredit(), "view"): 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 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['form'] = SogeCreditForm(self.request.POST or None) return context 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')