diff --git a/apps/treasury/__init__.py b/apps/treasury/__init__.py index e69de29b..c9c6150e 100644 --- a/apps/treasury/__init__.py +++ b/apps/treasury/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'treasury.apps.TreasuryConfig' diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py new file mode 100644 index 00000000..2d2fbbef --- /dev/null +++ b/apps/treasury/admin.py @@ -0,0 +1,16 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib import admin + +from treasury.models import Billing, Product + + +@admin.register(Billing) +class BillingAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'subject', 'acquitted', ) + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('designation', 'quantity', 'amount', ) diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py new file mode 100644 index 00000000..acf2eb7c --- /dev/null +++ b/apps/treasury/forms.py @@ -0,0 +1,29 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from crispy_forms.helper import FormHelper +from django import forms + +from .models import Billing, Product + + +class BillingForm(forms.ModelForm): + class Meta: + model = Billing + fields = '__all__' + + +ProductFormSet = forms.inlineformset_factory( + Billing, + Product, + fields='__all__', + extra=1, +) + +class ProductFormSetHelper(FormHelper): + def __init__(self, form=None): + super().__init__(form) + self.form_tag = False + self.form_method = 'POST' + self.form_class = 'form-inline' + self.template = 'bootstrap4/table_inline_formset.html' \ No newline at end of file diff --git a/apps/treasury/models.py b/apps/treasury/models.py index 81707f3f..7e3b5eba 100644 --- a/apps/treasury/models.py +++ b/apps/treasury/models.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db import models +from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ @@ -122,6 +123,14 @@ class Product(models.Model): verbose_name=_("Unit price") ) + @property + def amount_euros(self): + return self.amount / 100 + @property def total(self): return self.quantity * self.amount + + @property + def total_euros(self): + return self.total / 100 diff --git a/apps/treasury/tables.py b/apps/treasury/tables.py index 30cbb2e7..af80562a 100644 --- a/apps/treasury/tables.py +++ b/apps/treasury/tables.py @@ -14,7 +14,10 @@ class BillingTable(tables.Table): args=[A("pk")], accessor="pk", text="", - attrs={'a': {'class': 'fa fa-file-pdf-o'}}) + attrs={ + 'a': {'class': 'fa fa-file-pdf-o'}, + 'td': {'data-turbolinks': 'false'} + }) class Meta: attrs = { diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 1844167b..1fb4bcf0 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -6,15 +6,20 @@ import shutil import subprocess from tempfile import mkdtemp +from crispy_forms.helper import FormHelper from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q from django.http import HttpResponse from django.template.loader import render_to_string +from django.urls import reverse_lazy from django.views.generic import CreateView, UpdateView from django.views.generic.base import View from django_tables2 import SingleTableView + from note_kfet.settings.base import BASE_DIR -from .models import Billing +from .forms import BillingForm, ProductFormSet, ProductFormSetHelper +from .models import Billing, Product from .tables import BillingTable @@ -23,8 +28,36 @@ class BillingCreateView(LoginRequiredMixin, CreateView): Create Billing """ model = Billing - fields = '__all__' - # form_class = ClubForm + form_class = BillingForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = context['form'] + form.helper = FormHelper() + form.helper.form_tag = False + 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) + if formset.is_valid(): + for f in formset: + 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:billing') class BillingListView(LoginRequiredMixin, SingleTableView): @@ -40,8 +73,42 @@ class BillingUpdateView(LoginRequiredMixin, UpdateView): Create Billing """ model = Billing - fields = '__all__' - # form_class = BillingForm + form_class = BillingForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + form = context['form'] + form.helper = FormHelper() + form.helper.form_tag = False + 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 = [] + if formset.is_valid(): + for f in formset: + if f.is_valid() and f.instance.designation: + if type(f.instance.pk) == 'number' and f.instance.pk <= 0: + f.instance.pk = None + f.save() + f.instance.save() + saved.append(f.instance.pk) + else: + f.instance = None + + Product.objects.filter(~Q(pk__in=saved), billing=form.instance).delete() + + return ret + + def get_success_url(self): + return reverse_lazy('treasury:billing') class BillingRenderView(LoginRequiredMixin, View): @@ -52,10 +119,11 @@ class BillingRenderView(LoginRequiredMixin, View): def get(self, request, **kwargs): pk = kwargs["pk"] billing = Billing.objects.get(pk=pk) + products = Product.objects.filter(billing=billing).all() billing.description = billing.description.replace("\n", "\\newline\n") billing.address = billing.address.replace("\n", "\\newline\n") - tex = render_to_string("treasury/billing_sample.tex", dict(obj=billing)) + tex = render_to_string("treasury/billing_sample.tex", dict(obj=billing, products=products)) try: os.mkdir(BASE_DIR + "/tmp") except FileExistsError: diff --git a/static/js/dynamic-formset.js b/static/js/dynamic-formset.js index 87edfaae..c6ff3328 100644 --- a/static/js/dynamic-formset.js +++ b/static/js/dynamic-formset.js @@ -1,5 +1,5 @@ /** - * jQuery Formset 1.3-pre + * jQuery Formset 1.5-pre * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) * @requires jQuery 1.2.6 or later * @@ -55,19 +55,26 @@ insertDeleteLink = function(row) { var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); - if (row.is('TR')) { + + var delButtonHTML = '' + options.deleteText +''; + if (options.deleteContainerClass) { + // If we have a specific container for the remove button, + // place it as the last child of that container: + row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML); + } else if (row.is('TR')) { // If the forms are laid out in table rows, insert // the remove button into the last table cell: - row.children(':last').append('' + options.deleteText + ''); + row.children('td:last').append(delButtonHTML); } else if (row.is('UL') || row.is('OL')) { // If they're laid out as an ordered/unordered list, // insert an