Add products on billings

This commit is contained in:
Yohann D'ANELLO 2020-03-21 16:49:18 +01:00
parent 02ac33d143
commit a772cea760
9 changed files with 245 additions and 30 deletions

View File

@ -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'

16
apps/treasury/admin.py Normal file
View File

@ -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', )

29
apps/treasury/forms.py Normal file
View File

@ -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'

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models from django.db import models
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -122,6 +123,14 @@ class Product(models.Model):
verbose_name=_("Unit price") verbose_name=_("Unit price")
) )
@property
def amount_euros(self):
return self.amount / 100
@property @property
def total(self): def total(self):
return self.quantity * self.amount return self.quantity * self.amount
@property
def total_euros(self):
return self.total / 100

View File

@ -14,7 +14,10 @@ class BillingTable(tables.Table):
args=[A("pk")], args=[A("pk")],
accessor="pk", accessor="pk",
text="", text="",
attrs={'a': {'class': 'fa fa-file-pdf-o'}}) attrs={
'a': {'class': 'fa fa-file-pdf-o'},
'td': {'data-turbolinks': 'false'}
})
class Meta: class Meta:
attrs = { attrs = {

View File

@ -6,15 +6,20 @@ import shutil
import subprocess import subprocess
from tempfile import mkdtemp from tempfile import mkdtemp
from crispy_forms.helper import FormHelper
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.template.loader import render_to_string 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 import CreateView, UpdateView
from django.views.generic.base import View from django.views.generic.base import View
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from note_kfet.settings.base import BASE_DIR 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 from .tables import BillingTable
@ -23,8 +28,36 @@ class BillingCreateView(LoginRequiredMixin, CreateView):
Create Billing Create Billing
""" """
model = Billing model = Billing
fields = '__all__' form_class = BillingForm
# form_class = ClubForm
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): class BillingListView(LoginRequiredMixin, SingleTableView):
@ -40,8 +73,42 @@ class BillingUpdateView(LoginRequiredMixin, UpdateView):
Create Billing Create Billing
""" """
model = 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): class BillingRenderView(LoginRequiredMixin, View):
@ -52,10 +119,11 @@ class BillingRenderView(LoginRequiredMixin, View):
def get(self, request, **kwargs): def get(self, request, **kwargs):
pk = kwargs["pk"] pk = kwargs["pk"]
billing = Billing.objects.get(pk=pk) billing = Billing.objects.get(pk=pk)
products = Product.objects.filter(billing=billing).all()
billing.description = billing.description.replace("\n", "\\newline\n") billing.description = billing.description.replace("\n", "\\newline\n")
billing.address = billing.address.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: try:
os.mkdir(BASE_DIR + "/tmp") os.mkdir(BASE_DIR + "/tmp")
except FileExistsError: except FileExistsError:

View File

@ -1,5 +1,5 @@
/** /**
* jQuery Formset 1.3-pre * jQuery Formset 1.5-pre
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later * @requires jQuery 1.2.6 or later
* *
@ -55,19 +55,26 @@
insertDeleteLink = function(row) { insertDeleteLink = function(row) {
var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.');
if (row.is('TR')) {
var delButtonHTML = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>';
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 // If the forms are laid out in table rows, insert
// the remove button into the last table cell: // the remove button into the last table cell:
row.children(':last').append('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + '</a>'); row.children('td:last').append(delButtonHTML);
} else if (row.is('UL') || row.is('OL')) { } else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list, // If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item: // insert an <li> after the last list item:
row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a></li>'); row.append('<li>' + delButtonHTML + '</li>');
} else { } else {
// Otherwise, just insert the remove button as the // Otherwise, just insert the remove button as the
// last child element of the form's container: // last child element of the form's container:
row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>'); row.append(delButtonHTML);
} }
// Check if we're under the minimum number of forms - not to display delete link at rendering // Check if we're under the minimum number of forms - not to display delete link at rendering
if (!showDeleteLinks()){ if (!showDeleteLinks()){
row.find('a.' + delCssSelector).hide(); row.find('a.' + delCssSelector).hide();
@ -156,6 +163,7 @@
} else { } else {
// Otherwise, use the last form in the formset; this works much better if you've got // Otherwise, use the last form in the formset; this works much better if you've got
// extra (>= 1) forms (thnaks to justhamade for pointing this out): // extra (>= 1) forms (thnaks to justhamade for pointing this out):
if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide();
template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id'); template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
template.find('input:hidden[id $= "-DELETE"]').remove(); template.find('input:hidden[id $= "-DELETE"]').remove();
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion): // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
@ -173,21 +181,28 @@
// FIXME: Perhaps using $.data would be a better idea? // FIXME: Perhaps using $.data would be a better idea?
options.formTemplate = template; options.formTemplate = template;
if ($$.is('TR')) { var addButtonHTML = '<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>';
if (options.addContainerClass) {
// If we have a specific container for the "add" button,
// place it as the last child of that container:
var addContainer = $('[class*="' + options.addContainerClass + '"');
addContainer.append(addButtonHTML);
addButton = addContainer.find('[class="' + options.addCssClass + '"]');
} else if ($$.is('TR')) {
// If forms are laid out as table rows, insert the // If forms are laid out as table rows, insert the
// "add" button in a new table row: // "add" button in a new table row:
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :| var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
buttonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>') buttonRow = $('<tr><td colspan="' + numCols + '">' + addButtonHTML + '</tr>').addClass(options.formCssClass + '-add');
.addClass(options.formCssClass + '-add');
$$.parent().append(buttonRow); $$.parent().append(buttonRow);
if (hideAddButton) buttonRow.hide();
addButton = buttonRow.find('a'); addButton = buttonRow.find('a');
} else { } else {
// Otherwise, insert it immediately after the last form: // Otherwise, insert it immediately after the last form:
$$.filter(':last').after('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>'); $$.filter(':last').after(addButtonHTML);
addButton = $$.filter(':last').next(); addButton = $$.filter(':last').next();
if (hideAddButton) addButton.hide();
} }
if (hideAddButton) addButton.hide();
addButton.click(function() { addButton.click(function() {
var formCount = parseInt(totalForms.val()), var formCount = parseInt(totalForms.val()),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'), row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
@ -220,12 +235,15 @@
formTemplate: null, // The jQuery selection cloned to generate new form instances formTemplate: null, // The jQuery selection cloned to generate new form instances
addText: 'add another', // Text for the add link addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link deleteText: 'remove', // Text for the delete link
addCssClass: '', // CSS class applied to the add link addContainerClass: null, // Container CSS class for the add link
deleteCssClass: '', // CSS class applied to the delete link deleteContainerClass: null, // Container CSS class for the delete link
addCssClass: 'add-row', // CSS class applied to the add link
deleteCssClass: 'delete-row', // CSS class applied to the delete link
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
added: null, // Function called each time a new form is added added: null, // Function called each time a new form is added
removed: null // Function called each time a form is deleted removed: null, // Function called each time a form is deleted
hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button)
}; };
})(jQuery); })(jQuery);

View File

@ -3,10 +3,78 @@
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block content %} {% block content %}
<p><a class="btn btn-default" href="{% url 'treasury:billing' %}">{% trans "Billings list" %}</a></p> <p><a class="btn btn-default" href="{% url 'treasury:billing' %}">{% trans "Billings list" %}</a></p>
<form method="post"> <form method="post" action="" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
{{form|crispy}} {% crispy form %}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> {{ formset.management_form }}
</form> <table class="table table-condensed table-striped">
{% for form in formset %}
{% if forloop.first %}
<thead><tr>
<th>{{ form.designation.label }}<span class="asteriskField">*</span></th>
<th>{{ form.quantity.label }}<span class="asteriskField">*</span></th>
<th>{{ form.amount.label }}<span class="asteriskField">*</span></th>
</tr></thead>
<tbody id="form_body" >
{% endif %}
<tr class="row-formset">
<td>{{ form.designation }}</td>
<td>{{ form.quantity }} </td>
<td>{{ form.amount }}</td>
{{ form.billing }}
{{ form.id }}
</tr>
{% endfor %}
</tbody>
</table>
<div class="btn-group btn-block" role="group">
<button type="button" id="add_more" class="btn btn-primary">{% trans "Add product" %}</button>
<button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove product" %}</button>
</div>
<div class="btn-block">
<button type="submit" class="btn btn-block btn-primary">{% trans "Submit" %}</button>
</div>
</form>
<div id="empty_form" style="display: none;">
<table class='no_error'>
<tbody id="for_real">
<tr class="row-formset">
<td>{{ formset.empty_form.designation }}</td>
<td>{{ formset.empty_form.quantity }} </td>
<td>{{ formset.empty_form.amount }}</td>
{{ formset.empty_form.billing }}
{{ formset.empty_form.id }}
</tr>
</tbody>
</table>
</div>
{% endblock %}
{% block extrajavascript %}
<script src="{% static 'js/dynamic-formset.js' %}"></script>
<script>
IDS = {};
$("#id_product_set-TOTAL_FORMS").val($(".row-formset").length - 1);
$('#add_more').click(function() {
var form_idx = $('#id_product_set-TOTAL_FORMS').val();
$('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx));
$('#id_product_set-TOTAL_FORMS').val(parseInt(form_idx) + 1);
$('#id_product_set-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]);
});
$('#remove_one').click(function() {
let form_idx = $('#id_product_set-TOTAL_FORMS').val();
if (form_idx > 0) {
IDS[parseInt(form_idx) - 1] = $('#id_product_set-' + (parseInt(form_idx) - 1) + '-id').val();
$('#form_body tr:last-child').remove();
$('#id_product_set-TOTAL_FORMS').val(parseInt(form_idx) - 1);
}
});
</script>
{% endblock %} {% endblock %}

View File

@ -95,8 +95,8 @@
% Liste des produits facturés : Désignation, quantité, prix unitaire HT % Liste des produits facturés : Désignation, quantité, prix unitaire HT
{% for product in obj.products %} {% for product in products %}
\AjouterProduit{ {{product.designation|safe}} } { {{product.quantity|safe}} } { {{product.amount|safe}}} { {{product.total|safe}}} \AjouterProduit{ {{product.designation|safe}}} { {{product.quantity|safe}}} { {{product.amount_euros|safe}}} { {{product.total_euros|safe}}}
{% endfor %} {% endfor %}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@ -112,7 +112,7 @@
\renewcommand{\headrulewidth}{0pt} \renewcommand{\headrulewidth}{0pt}
\cfoot{ \cfoot{
\small{\MonNom ~--~ \MonAdresseRue \MonAdresseVille ~--~ Telephone : +33(0)6 89 88 56 50\newline \small{\MonNom ~--~ \MonAdresseRue ~ \MonAdresseVille ~--~ Téléphone : +33(0)6 89 88 56 50\newline
Site web : bde.ens-cachan.fr ~--~ E-mail : tresorerie.bde@lists.crans.org \newline Numéro SIRET : 399 485 838 00011 Site web : bde.ens-cachan.fr ~--~ E-mail : tresorerie.bde@lists.crans.org \newline Numéro SIRET : 399 485 838 00011
} }
} }