Skip to content
Snippets Groups Projects
Commit a33d373f authored by ynerant's avatar ynerant
Browse files

Comment code

parent 093e585b
No related branches found
No related tags found
1 merge request!61Tresorerie
Pipeline #8016 passed with stages
in 3 minutes and 39 seconds
......@@ -5,7 +5,6 @@ from django.apps import AppConfig
from django.db.models import Q
from django.db.models.signals import post_save, post_migrate
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial
class TreasuryConfig(AppConfig):
......@@ -18,7 +17,7 @@ class TreasuryConfig(AppConfig):
"""
from . import signals
from note.models import SpecialTransaction
from note.models import SpecialTransaction, NoteSpecial
from treasury.models import SpecialTransactionProxy
post_save.connect(signals.save_special_transaction, sender=SpecialTransaction)
......
......@@ -12,6 +12,11 @@ from .models import Invoice, Product, Remittance, SpecialTransactionProxy
class InvoiceForm(forms.ModelForm):
"""
Create and generate invoices.
"""
# Django forms don't support date fields. We have to add it manually
date = forms.DateField(
initial=datetime.date.today,
widget=forms.TextInput(attrs={'type': 'date'})
......@@ -25,6 +30,8 @@ class InvoiceForm(forms.ModelForm):
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(
Invoice,
Product,
......@@ -34,6 +41,10 @@ ProductFormSet = forms.inlineformset_factory(
class ProductFormSetHelper(FormHelper):
"""
Specify some template informations for the product form.
"""
def __init__(self, form=None):
super().__init__(form)
self.form_tag = False
......@@ -43,24 +54,33 @@ class ProductFormSetHelper(FormHelper):
class RemittanceForm(forms.ModelForm):
"""
Create remittances.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
# We can't update the type of the remittance once created.
if self.instance.pk:
self.fields["remittance_type"].disabled = True
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:
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
if self.instance.transactions:
self.helper.add_input(Submit("close", _("Close"), css_class='btn btn-success'))
else:
# If the remittance is closed, we can't change anything
self.fields["comment"].disabled = True
self.fields["comment"].required = False
def clean(self):
# We can't update anything if the remittance is already closed.
if self.instance.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:
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:
self.instance.closed = True
self.cleaned_data["closed"] = True
......@@ -81,6 +102,11 @@ class RemittanceForm(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"))
first_name = forms.Field(label=_("First name"))
......@@ -92,21 +118,34 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
# Add submit button
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
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.clean()
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.clean()
def clean_bank(self):
"""
Replace the bank in the information of the transaction.
"""
self.instance.transaction.bank = self.data.get("bank")
self.instance.transaction.clean()
def clean_amount(self):
"""
Replace the amount of the transaction.
"""
self.instance.transaction.amount = self.data.get("amount")
self.instance.transaction.clean()
......
......@@ -10,7 +10,7 @@ from note.models import NoteSpecial, SpecialTransaction
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(
......@@ -62,7 +62,7 @@ class Invoice(models.Model):
class Product(models.Model):
"""
Product that appear on an invoice.
Product that appears on an invoice.
"""
invoice = models.ForeignKey(
......@@ -138,18 +138,28 @@ class Remittance(models.Model):
@property
def transactions(self):
"""
:return: Transactions linked to this remittance.
"""
if not self.pk:
return SpecialTransaction.objects.none()
return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self)
def count(self):
"""
Linked transactions count.
"""
return self.transactions.count()
@property
def amount(self):
"""
Total amount of the remittance.
"""
return sum(transaction.total for transaction in self.transactions.all())
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# Check if all transactions have the right type.
if self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
raise ValidationError("All transactions in a remittance must have the same type")
......
......@@ -11,6 +11,9 @@ from .models import Invoice, Remittance
class InvoiceTable(tables.Table):
"""
List all invoices.
"""
id = tables.LinkColumn("treasury:invoice_update",
args=[A("pk")],
text=lambda record: _("Invoice #{:d}").format(record.id), )
......@@ -35,6 +38,10 @@ class InvoiceTable(tables.Table):
class RemittanceTable(tables.Table):
"""
List all remittances.
"""
count = tables.Column(verbose_name=_("Transaction count"))
amount = tables.Column(verbose_name=_("Amount"))
......@@ -60,6 +67,11 @@ class RemittanceTable(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",
verbose_name=_("Remittance"),
args=[A("specialtransactionproxy.pk")],
......
......@@ -8,11 +8,13 @@ from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, Invoic
app_name = 'treasury'
urlpatterns = [
# Invoice app paths
path('invoice/', InvoiceListView.as_view(), name='invoice_list'),
path('invoice/create/', InvoiceCreateView.as_view(), name='invoice_create'),
path('invoice/<int:pk>/', InvoiceUpdateView.as_view(), name='invoice_update'),
path('invoice/render/<int:pk>/', InvoiceRenderView.as_view(), name='invoice_render'),
# Remittance app paths
path('remittance/', RemittanceListView.as_view(), name='remittance_list'),
path('remittance/create/', RemittanceCreateView.as_view(), name='remittance_create'),
path('remittance/<int:pk>/', RemittanceUpdateView.as_view(), name='remittance_update'),
......
......@@ -21,7 +21,7 @@ from note.models import SpecialTransaction, NoteSpecial
from note_kfet.settings.base import BASE_DIR
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
......@@ -34,9 +34,12 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
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()
......@@ -48,6 +51,8 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
ret = super().form_valid(form)
kwargs = {}
# The user type amounts in cents. We convert it in euros.
for key in self.request.POST:
value = self.request.POST[key]
if key.endswith("amount") and value:
......@@ -55,9 +60,11 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
elif value:
kwargs[key] = value
# For each product, we save it
formset = ProductFormSet(kwargs, 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()
......@@ -87,10 +94,14 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
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()
......@@ -102,6 +113,7 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
ret = super().form_valid(form)
kwargs = {}
# The user type amounts in cents. We convert it in euros.
for key in self.request.POST:
value = self.request.POST[key]
if key.endswith("amount") and value:
......@@ -111,14 +123,17 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
formset = ProductFormSet(kwargs, 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
......@@ -129,7 +144,7 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
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):
......@@ -137,6 +152,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
invoice = Invoice.objects.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"
......@@ -147,13 +163,17 @@ class InvoiceRenderView(LoginRequiredMixin, View):
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:
......@@ -161,21 +181,27 @@ class InvoiceRenderView(LoginRequiredMixin, View):
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=invoice-{:d}.pdf".format(pk)
except IOError as e:
raise e
finally:
# Delete all temporary files
shutil.rmtree(tmp_dir)
return response
......@@ -211,6 +237,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all())
ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all())
ctx["special_transactions_no_remittance"] = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance=None).all(),
......@@ -246,6 +273,10 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
"""
Attach a special transaction to a remittance
"""
model = SpecialTransactionProxy
form_class = LinkTransactionToRemittanceForm
......@@ -267,10 +298,15 @@ class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
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.")
......
......@@ -1004,7 +1004,7 @@ msgid "Transfers with opened remittances"
msgstr ""
#: 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 ""
#: templates/treasury/remittance_list.html:54
......
......@@ -1003,11 +1003,11 @@ msgstr "Il n'y a pas de transactions sans remise associée."
#: templates/treasury/remittance_list.html:43
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
msgid "There is no transaction without an opened linked remittance."
msgstr "Il n'y a pas de transaction sans remise ouverte associée."
msgid "There is no transaction with an opened linked remittance."
msgstr "Il n'y a pas de transaction associée à une remise ouverte."
#: templates/treasury/remittance_list.html:54
msgid "Closed remittances"
......
......@@ -6,22 +6,29 @@
<p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p>
<form method="post" action="">
{% csrf_token %}
{# Render the invoice 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 }}
<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">
{# Fill initial data #}
{% 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>
{# Use custom input for amount, with the € symbol #}
<div class="input-group">
<input type="number" name="product_set-{{ forloop.counter0 }}-amount" min="0" step="0.01"
id="id_product_set-{{ forloop.counter0 }}-amount"
......@@ -31,13 +38,15 @@
</div>
</div>
</td>
{# These fields are hidden but handled by the formset to link the id and the invoice id #}
{{ form.invoice }}
{{ form.id }}
</tr>
{% endfor %}
</tbody>
</tr>
{% endfor %}
</tbody>
</table>
{# Display buttons to add and remove products #}
<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>
......@@ -49,43 +58,44 @@
</form>
<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'>
<tbody id="for_real">
<tr class="row-formset">
<td>{{ formset.empty_form.designation }}</td>
<td>{{ formset.empty_form.quantity }} </td>
<td>
<div class="input-group">
<input type="number" name="product_set-__prefix__-amount" min="0" step="0.01"
id="id_product_set-__prefix__-amount">
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
<tr class="row-formset">
<td>{{ formset.empty_form.designation }}</td>
<td>{{ formset.empty_form.quantity }} </td>
<td>
<div class="input-group">
<input type="number" name="product_set-__prefix__-amount" min="0" step="0.01"
id="id_product_set-__prefix__-amount">
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</td>
{{ formset.empty_form.invoice }}
{{ formset.empty_form.id }}
</tr>
</div>
</td>
{{ formset.empty_form.invoice }}
{{ formset.empty_form.id }}
</tr>
</tbody>
</table>
</div>
{% endblock %}
{% block extrajavascript %}
<script src="{% static 'js/dynamic-formset.js' %}"></script>
<script>
{# Script that handles add and remove lines #}
IDS = {};
$("#id_product_set-TOTAL_FORMS").val($(".row-formset").length - 1);
$('#add_more').click(function() {
$('#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() {
$('#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();
......
......@@ -45,7 +45,7 @@
{% render_table special_transactions_with_remittance %}
{% else %}
<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>
{% endif %}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment