1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Comment code

This commit is contained in:
Yohann D'ANELLO 2020-03-24 20:22:15 +01:00
parent 093e585b79
commit a33d373f6e
10 changed files with 152 additions and 44 deletions

View File

@ -5,7 +5,6 @@ from django.apps import AppConfig
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import post_save, post_migrate from django.db.models.signals import post_save, post_migrate
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial
class TreasuryConfig(AppConfig): class TreasuryConfig(AppConfig):
@ -18,7 +17,7 @@ class TreasuryConfig(AppConfig):
""" """
from . import signals from . import signals
from note.models import SpecialTransaction from note.models import SpecialTransaction, NoteSpecial
from treasury.models import SpecialTransactionProxy from treasury.models import SpecialTransactionProxy
post_save.connect(signals.save_special_transaction, sender=SpecialTransaction) post_save.connect(signals.save_special_transaction, sender=SpecialTransaction)

View File

@ -12,6 +12,11 @@ from .models import Invoice, Product, Remittance, SpecialTransactionProxy
class InvoiceForm(forms.ModelForm): class InvoiceForm(forms.ModelForm):
"""
Create and generate invoices.
"""
# Django forms don't support date fields. We have to add it manually
date = forms.DateField( date = forms.DateField(
initial=datetime.date.today, initial=datetime.date.today,
widget=forms.TextInput(attrs={'type': 'date'}) widget=forms.TextInput(attrs={'type': 'date'})
@ -25,6 +30,8 @@ class InvoiceForm(forms.ModelForm):
exclude = ('bde', ) exclude = ('bde', )
# Add a subform per product in the invoice form, and manage correctly the link between the invoice and
# its products. The FormSet will search automatically the ForeignKey in the Product model.
ProductFormSet = forms.inlineformset_factory( ProductFormSet = forms.inlineformset_factory(
Invoice, Invoice,
Product, Product,
@ -34,6 +41,10 @@ ProductFormSet = forms.inlineformset_factory(
class ProductFormSetHelper(FormHelper): class ProductFormSetHelper(FormHelper):
"""
Specify some template informations for the product form.
"""
def __init__(self, form=None): def __init__(self, form=None):
super().__init__(form) super().__init__(form)
self.form_tag = False self.form_tag = False
@ -43,24 +54,33 @@ class ProductFormSetHelper(FormHelper):
class RemittanceForm(forms.ModelForm): class RemittanceForm(forms.ModelForm):
"""
Create remittances.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
# We can't update the type of the remittance once created.
if self.instance.pk: if self.instance.pk:
self.fields["remittance_type"].disabled = True self.fields["remittance_type"].disabled = True
self.fields["remittance_type"].required = False self.fields["remittance_type"].required = False
# We display the submit button iff the remittance is open,
# the close button iff it is open and has a linked transaction
if not self.instance.closed: if not self.instance.closed:
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
if self.instance.transactions: if self.instance.transactions:
self.helper.add_input(Submit("close", _("Close"), css_class='btn btn-success')) self.helper.add_input(Submit("close", _("Close"), css_class='btn btn-success'))
else: else:
# If the remittance is closed, we can't change anything
self.fields["comment"].disabled = True self.fields["comment"].disabled = True
self.fields["comment"].required = False self.fields["comment"].required = False
def clean(self): def clean(self):
# We can't update anything if the remittance is already closed.
if self.instance.closed: if self.instance.closed:
self.add_error("comment", _("Remittance is already closed.")) self.add_error("comment", _("Remittance is already closed."))
@ -69,6 +89,7 @@ class RemittanceForm(forms.ModelForm):
if self.instance.pk and cleaned_data.get("remittance_type") != self.instance.remittance_type: if self.instance.pk and cleaned_data.get("remittance_type") != self.instance.remittance_type:
self.add_error("remittance_type", _("You can't change the type of the remittance.")) self.add_error("remittance_type", _("You can't change the type of the remittance."))
# The close button is manually handled
if "close" in self.data: if "close" in self.data:
self.instance.closed = True self.instance.closed = True
self.cleaned_data["closed"] = True self.cleaned_data["closed"] = True
@ -81,6 +102,11 @@ class RemittanceForm(forms.ModelForm):
class LinkTransactionToRemittanceForm(forms.ModelForm): class LinkTransactionToRemittanceForm(forms.ModelForm):
"""
Attach a special transaction to a remittance.
"""
# Since we use a proxy model for special transactions, we add manually the fields related to the transaction
last_name = forms.CharField(label=_("Last name")) last_name = forms.CharField(label=_("Last name"))
first_name = forms.Field(label=_("First name")) first_name = forms.Field(label=_("First name"))
@ -92,21 +118,34 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
# Add submit button
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
def clean_last_name(self): def clean_last_name(self):
"""
Replace the first name in the information of the transaction.
"""
self.instance.transaction.last_name = self.data.get("last_name") self.instance.transaction.last_name = self.data.get("last_name")
self.instance.transaction.clean() self.instance.transaction.clean()
def clean_first_name(self): def clean_first_name(self):
"""
Replace the last name in the information of the transaction.
"""
self.instance.transaction.first_name = self.data.get("first_name") self.instance.transaction.first_name = self.data.get("first_name")
self.instance.transaction.clean() self.instance.transaction.clean()
def clean_bank(self): def clean_bank(self):
"""
Replace the bank in the information of the transaction.
"""
self.instance.transaction.bank = self.data.get("bank") self.instance.transaction.bank = self.data.get("bank")
self.instance.transaction.clean() self.instance.transaction.clean()
def clean_amount(self): def clean_amount(self):
"""
Replace the amount of the transaction.
"""
self.instance.transaction.amount = self.data.get("amount") self.instance.transaction.amount = self.data.get("amount")
self.instance.transaction.clean() self.instance.transaction.clean()

View File

@ -10,7 +10,7 @@ from note.models import NoteSpecial, SpecialTransaction
class Invoice(models.Model): class Invoice(models.Model):
""" """
An invoice model that can generate a true invoice An invoice model that can generates a true invoice.
""" """
id = models.PositiveIntegerField( id = models.PositiveIntegerField(
@ -62,7 +62,7 @@ class Invoice(models.Model):
class Product(models.Model): class Product(models.Model):
""" """
Product that appear on an invoice. Product that appears on an invoice.
""" """
invoice = models.ForeignKey( invoice = models.ForeignKey(
@ -138,18 +138,28 @@ class Remittance(models.Model):
@property @property
def transactions(self): def transactions(self):
"""
:return: Transactions linked to this remittance.
"""
if not self.pk:
return SpecialTransaction.objects.none()
return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self) return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self)
def count(self): def count(self):
"""
Linked transactions count.
"""
return self.transactions.count() return self.transactions.count()
@property @property
def amount(self): def amount(self):
"""
Total amount of the remittance.
"""
return sum(transaction.total for transaction in self.transactions.all()) return sum(transaction.total for transaction in self.transactions.all())
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
update_fields=None): # Check if all transactions have the right type.
if self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): if self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
raise ValidationError("All transactions in a remittance must have the same type") raise ValidationError("All transactions in a remittance must have the same type")

View File

@ -11,6 +11,9 @@ from .models import Invoice, Remittance
class InvoiceTable(tables.Table): class InvoiceTable(tables.Table):
"""
List all invoices.
"""
id = tables.LinkColumn("treasury:invoice_update", id = tables.LinkColumn("treasury:invoice_update",
args=[A("pk")], args=[A("pk")],
text=lambda record: _("Invoice #{:d}").format(record.id), ) text=lambda record: _("Invoice #{:d}").format(record.id), )
@ -35,6 +38,10 @@ class InvoiceTable(tables.Table):
class RemittanceTable(tables.Table): class RemittanceTable(tables.Table):
"""
List all remittances.
"""
count = tables.Column(verbose_name=_("Transaction count")) count = tables.Column(verbose_name=_("Transaction count"))
amount = tables.Column(verbose_name=_("Amount")) amount = tables.Column(verbose_name=_("Amount"))
@ -60,6 +67,11 @@ class RemittanceTable(tables.Table):
class SpecialTransactionTable(tables.Table): class SpecialTransactionTable(tables.Table):
"""
List special credit transactions that are (or not, following the queryset) attached to a remittance.
"""
# Display add and remove buttons. Use the `exclude` field to select what is needed.
remittance_add = tables.LinkColumn("treasury:link_transaction", remittance_add = tables.LinkColumn("treasury:link_transaction",
verbose_name=_("Remittance"), verbose_name=_("Remittance"),
args=[A("specialtransactionproxy.pk")], args=[A("specialtransactionproxy.pk")],

View File

@ -8,11 +8,13 @@ from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, Invoic
app_name = 'treasury' app_name = 'treasury'
urlpatterns = [ urlpatterns = [
# Invoice app paths
path('invoice/', InvoiceListView.as_view(), name='invoice_list'), path('invoice/', InvoiceListView.as_view(), name='invoice_list'),
path('invoice/create/', InvoiceCreateView.as_view(), name='invoice_create'), path('invoice/create/', InvoiceCreateView.as_view(), name='invoice_create'),
path('invoice/<int:pk>/', InvoiceUpdateView.as_view(), name='invoice_update'), path('invoice/<int:pk>/', InvoiceUpdateView.as_view(), name='invoice_update'),
path('invoice/render/<int:pk>/', InvoiceRenderView.as_view(), name='invoice_render'), path('invoice/render/<int:pk>/', InvoiceRenderView.as_view(), name='invoice_render'),
# Remittance app paths
path('remittance/', RemittanceListView.as_view(), name='remittance_list'), path('remittance/', RemittanceListView.as_view(), name='remittance_list'),
path('remittance/create/', RemittanceCreateView.as_view(), name='remittance_create'), path('remittance/create/', RemittanceCreateView.as_view(), name='remittance_create'),
path('remittance/<int:pk>/', RemittanceUpdateView.as_view(), name='remittance_update'), path('remittance/<int:pk>/', RemittanceUpdateView.as_view(), name='remittance_update'),

View File

@ -21,7 +21,7 @@ from note.models import SpecialTransaction, NoteSpecial
from note_kfet.settings.base import BASE_DIR from note_kfet.settings.base import BASE_DIR
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
from .models import Invoice, Product, Remittance, SpecialTransactionProxy, RemittanceType from .models import Invoice, Product, Remittance, SpecialTransactionProxy
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
@ -34,9 +34,12 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
form = context['form'] form = context['form']
form.helper = FormHelper() 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 form.helper.form_tag = False
# The formset handles the set of the products
form_set = ProductFormSet(instance=form.instance) form_set = ProductFormSet(instance=form.instance)
context['formset'] = form_set context['formset'] = form_set
context['helper'] = ProductFormSetHelper() context['helper'] = ProductFormSetHelper()
@ -48,6 +51,8 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
ret = super().form_valid(form) ret = super().form_valid(form)
kwargs = {} kwargs = {}
# The user type amounts in cents. We convert it in euros.
for key in self.request.POST: for key in self.request.POST:
value = self.request.POST[key] value = self.request.POST[key]
if key.endswith("amount") and value: if key.endswith("amount") and value:
@ -55,9 +60,11 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
elif value: elif value:
kwargs[key] = value kwargs[key] = value
# For each product, we save it
formset = ProductFormSet(kwargs, instance=form.instance) formset = ProductFormSet(kwargs, instance=form.instance)
if formset.is_valid(): if formset.is_valid():
for f in formset: 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: if f.is_valid() and f.instance.designation:
f.save() f.save()
f.instance.save() f.instance.save()
@ -87,10 +94,14 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
form = context['form'] form = context['form']
form.helper = FormHelper() 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 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 form.fields['date'].initial = form.instance.date
# The formset handles the set of the products
form_set = ProductFormSet(instance=form.instance) form_set = ProductFormSet(instance=form.instance)
context['formset'] = form_set context['formset'] = form_set
context['helper'] = ProductFormSetHelper() context['helper'] = ProductFormSetHelper()
@ -102,6 +113,7 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
ret = super().form_valid(form) ret = super().form_valid(form)
kwargs = {} kwargs = {}
# The user type amounts in cents. We convert it in euros.
for key in self.request.POST: for key in self.request.POST:
value = self.request.POST[key] value = self.request.POST[key]
if key.endswith("amount") and value: if key.endswith("amount") and value:
@ -111,14 +123,17 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
formset = ProductFormSet(kwargs, instance=form.instance) formset = ProductFormSet(kwargs, instance=form.instance)
saved = [] saved = []
# For each product, we save it
if formset.is_valid(): if formset.is_valid():
for f in formset: 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: if f.is_valid() and f.instance.designation:
f.save() f.save()
f.instance.save() f.instance.save()
saved.append(f.instance.pk) saved.append(f.instance.pk)
else: else:
f.instance = None f.instance = None
# Remove old products that weren't given in the form
Product.objects.filter(~Q(pk__in=saved), invoice=form.instance).delete() Product.objects.filter(~Q(pk__in=saved), invoice=form.instance).delete()
return ret return ret
@ -129,7 +144,7 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
class InvoiceRenderView(LoginRequiredMixin, View): class InvoiceRenderView(LoginRequiredMixin, View):
""" """
Render Invoice as generated PDF Render Invoice as a generated PDF with the given information and a LaTeX template
""" """
def get(self, request, **kwargs): def get(self, request, **kwargs):
@ -137,6 +152,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
invoice = Invoice.objects.get(pk=pk) invoice = Invoice.objects.get(pk=pk)
products = Product.objects.filter(invoice=invoice).all() products = Product.objects.filter(invoice=invoice).all()
# Informations of the BDE. Should be updated when the school will move.
invoice.place = "Cachan" invoice.place = "Cachan"
invoice.my_name = "BDE ENS Cachan" invoice.my_name = "BDE ENS Cachan"
invoice.my_address_street = "61 avenue du Président Wilson" invoice.my_address_street = "61 avenue du Président Wilson"
@ -147,13 +163,17 @@ class InvoiceRenderView(LoginRequiredMixin, View):
invoice.rib_key = 14 invoice.rib_key = 14
invoice.bic = "SOGEFRPP" invoice.bic = "SOGEFRPP"
# Replace line breaks with the LaTeX equivalent
invoice.description = invoice.description.replace("\r", "").replace("\n", "\\\\ ") invoice.description = invoice.description.replace("\r", "").replace("\n", "\\\\ ")
invoice.address = invoice.address.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)) tex = render_to_string("treasury/invoice_sample.tex", dict(obj=invoice, products=products))
try: try:
os.mkdir(BASE_DIR + "/tmp") os.mkdir(BASE_DIR + "/tmp")
except FileExistsError: except FileExistsError:
pass pass
# We render the file in a temporary directory
tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/") tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/")
try: try:
@ -161,21 +181,27 @@ class InvoiceRenderView(LoginRequiredMixin, View):
f.write(tex.encode("UTF-8")) f.write(tex.encode("UTF-8"))
del tex del tex
# The file has to be rendered twice
for _ in range(2): for _ in range(2):
error = subprocess.Popen( error = subprocess.Popen(
["pdflatex", "invoice-{}.tex".format(pk)], ["pdflatex", "invoice-{}.tex".format(pk)],
cwd=tmp_dir, cwd=tmp_dir,
stdin=open(os.devnull, "r"),
stderr=open(os.devnull, "wb"),
stdout=open(os.devnull, "wb"),
).wait() ).wait()
if error: if error:
raise IOError("An error attempted while generating a invoice (code=" + str(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() pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read()
response = HttpResponse(pdf, content_type="application/pdf") response = HttpResponse(pdf, content_type="application/pdf")
response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk) response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk)
except IOError as e: except IOError as e:
raise e raise e
finally: finally:
# Delete all temporary files
shutil.rmtree(tmp_dir) shutil.rmtree(tmp_dir)
return response return response
@ -211,6 +237,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all()) ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all())
ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all()) ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all())
ctx["special_transactions_no_remittance"] = SpecialTransactionTable( ctx["special_transactions_no_remittance"] = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance=None).all(), specialtransactionproxy__remittance=None).all(),
@ -246,6 +273,10 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
"""
Attach a special transaction to a remittance
"""
model = SpecialTransactionProxy model = SpecialTransactionProxy
form_class = LinkTransactionToRemittanceForm form_class = LinkTransactionToRemittanceForm
@ -267,10 +298,15 @@ class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View): class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View):
"""
Unlink a special transaction and its remittance
"""
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
pk = kwargs["pk"] pk = kwargs["pk"]
transaction = SpecialTransactionProxy.objects.get(pk=pk) transaction = SpecialTransactionProxy.objects.get(pk=pk)
# The remittance must be open (or inexistant)
if transaction.remittance and transaction.remittance.closed: if transaction.remittance and transaction.remittance.closed:
raise ValidationError("Remittance is already closed.") raise ValidationError("Remittance is already closed.")

View File

@ -1004,7 +1004,7 @@ msgid "Transfers with opened remittances"
msgstr "" msgstr ""
#: templates/treasury/remittance_list.html:48 #: templates/treasury/remittance_list.html:48
msgid "There is no transaction without an opened linked remittance." msgid "There is no transaction with an opened linked remittance."
msgstr "" msgstr ""
#: templates/treasury/remittance_list.html:54 #: templates/treasury/remittance_list.html:54

View File

@ -1003,11 +1003,11 @@ msgstr "Il n'y a pas de transactions sans remise associée."
#: templates/treasury/remittance_list.html:43 #: templates/treasury/remittance_list.html:43
msgid "Transfers with opened remittances" msgid "Transfers with opened remittances"
msgstr "Transactions avec remise associée ouverte" msgstr "Transactions associées à une remise ouverte"
#: templates/treasury/remittance_list.html:48 #: templates/treasury/remittance_list.html:48
msgid "There is no transaction without an opened linked remittance." msgid "There is no transaction with an opened linked remittance."
msgstr "Il n'y a pas de transaction sans remise ouverte associée." msgstr "Il n'y a pas de transaction associée à une remise ouverte."
#: templates/treasury/remittance_list.html:54 #: templates/treasury/remittance_list.html:54
msgid "Closed remittances" msgid "Closed remittances"

View File

@ -6,22 +6,29 @@
<p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p> <p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p>
<form method="post" action=""> <form method="post" action="">
{% csrf_token %} {% csrf_token %}
{# Render the invoice form #}
{% crispy form %} {% crispy form %}
{# The next part concerns the product formset #}
{# Generate some hidden fields that manage the number of products, and make easier the parsing #}
{{ formset.management_form }} {{ formset.management_form }}
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">
{# Fill initial data #}
{% for form in formset %} {% for form in formset %}
{% if forloop.first %} {% if forloop.first %}
<thead><tr> <thead>
<tr>
<th>{{ form.designation.label }}<span class="asteriskField">*</span></th> <th>{{ form.designation.label }}<span class="asteriskField">*</span></th>
<th>{{ form.quantity.label }}<span class="asteriskField">*</span></th> <th>{{ form.quantity.label }}<span class="asteriskField">*</span></th>
<th>{{ form.amount.label }}<span class="asteriskField">*</span></th> <th>{{ form.amount.label }}<span class="asteriskField">*</span></th>
</tr></thead> </tr>
</thead>
<tbody id="form_body"> <tbody id="form_body">
{% endif %} {% endif %}
<tr class="row-formset"> <tr class="row-formset">
<td>{{ form.designation }}</td> <td>{{ form.designation }}</td>
<td>{{ form.quantity }} </td> <td>{{ form.quantity }} </td>
<td> <td>
{# Use custom input for amount, with the € symbol #}
<div class="input-group"> <div class="input-group">
<input type="number" name="product_set-{{ forloop.counter0 }}-amount" min="0" step="0.01" <input type="number" name="product_set-{{ forloop.counter0 }}-amount" min="0" step="0.01"
id="id_product_set-{{ forloop.counter0 }}-amount" id="id_product_set-{{ forloop.counter0 }}-amount"
@ -31,6 +38,7 @@
</div> </div>
</div> </div>
</td> </td>
{# These fields are hidden but handled by the formset to link the id and the invoice id #}
{{ form.invoice }} {{ form.invoice }}
{{ form.id }} {{ form.id }}
</tr> </tr>
@ -38,6 +46,7 @@
</tbody> </tbody>
</table> </table>
{# Display buttons to add and remove products #}
<div class="btn-group btn-block" role="group"> <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="add_more" class="btn btn-primary">{% trans "Add product" %}</button>
<button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove product" %}</button> <button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove product" %}</button>
@ -49,6 +58,7 @@
</form> </form>
<div id="empty_form" style="display: none;"> <div id="empty_form" style="display: none;">
{# Hidden div that store an empty product form, to be copied into new forms #}
<table class='no_error'> <table class='no_error'>
<tbody id="for_real"> <tbody id="for_real">
<tr class="row-formset"> <tr class="row-formset">
@ -72,8 +82,8 @@
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script src="{% static 'js/dynamic-formset.js' %}"></script>
<script> <script>
{# Script that handles add and remove lines #}
IDS = {}; IDS = {};
$("#id_product_set-TOTAL_FORMS").val($(".row-formset").length - 1); $("#id_product_set-TOTAL_FORMS").val($(".row-formset").length - 1);

View File

@ -45,7 +45,7 @@
{% render_table special_transactions_with_remittance %} {% render_table special_transactions_with_remittance %}
{% else %} {% else %}
<div class="alert alert-warning"> <div class="alert alert-warning">
{% trans "There is no transaction without an opened linked remittance." %} {% trans "There is no transaction with an opened linked remittance." %}
</div> </div>
{% endif %} {% endif %}