8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-23 11:53:12 +00:00

Custom invoices.

This commit is contained in:
Hugo LEVY-FALK 2018-07-22 00:16:05 +02:00
parent 0527206eb4
commit d6091d117c
10 changed files with 526 additions and 139 deletions

View file

@ -46,7 +46,7 @@ from django.shortcuts import get_object_or_404
from re2o.field_permissions import FieldPermissionFormMixin from re2o.field_permissions import FieldPermissionFormMixin
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
from .models import Article, Paiement, Facture, Banque from .models import Article, Paiement, Facture, Banque, CustomInvoice
from .payment_methods import balance from .payment_methods import balance
@ -131,24 +131,13 @@ class SelectClubArticleForm(Form):
self.fields['article'].queryset = Article.find_allowed_articles(user) self.fields['article'].queryset = Article.find_allowed_articles(user)
# TODO : change Facture to Invoice class CustomInvoiceForm(FormRevMixin, ModelForm):
class NewFactureFormPdf(Form):
""" """
Form used to create a custom PDF invoice. Form used to create a custom invoice.
""" """
paid = forms.BooleanField(label=_l("Paid"), required=False) class Meta:
# TODO : change dest field to recipient model = CustomInvoice
dest = forms.CharField( fields = '__all__'
required=True,
max_length=255,
label=_l("Recipient")
)
# TODO : change chambre field to address
chambre = forms.CharField(
required=False,
max_length=10,
label=_l("Address")
)
class ArticleForm(FormRevMixin, ModelForm): class ArticleForm(FormRevMixin, ModelForm):

View file

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-07-21 20:01
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import re2o.field_permissions
import re2o.mixins
def reattribute_ids(apps, schema_editor):
Facture = apps.get_model('cotisations', 'Facture')
BaseInvoice = apps.get_model('cotisations', 'BaseInvoice')
for f in Facture.objects.all():
base = BaseInvoice.objects.create(id=f.pk, date=f.date)
f.baseinvoice_ptr = base
f.save()
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0030_custom_payment'),
]
operations = [
migrations.CreateModel(
name='BaseInvoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
],
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, re2o.field_permissions.FieldPermissionModelMixin, models.Model),
),
migrations.CreateModel(
name='CustomInvoice',
fields=[
('baseinvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice')),
('recipient', models.CharField(max_length=255, verbose_name='Recipient')),
('payment', models.CharField(max_length=255, verbose_name='Payment type')),
('address', models.CharField(max_length=255, verbose_name='Address')),
('paid', models.BooleanField(verbose_name='Paid')),
],
bases=('cotisations.baseinvoice',),
options={'permissions': (('view_custom_invoice', 'Can view a custom invoice'),)},
),
migrations.AddField(
model_name='facture',
name='baseinvoice_ptr',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='cotisations.BaseInvoice', null=True),
preserve_default=False,
),
migrations.RunPython(reattribute_ids),
migrations.AlterField(
model_name='vente',
name='facture',
field=models.ForeignKey(on_delete=models.CASCADE, verbose_name='Invoice', to='cotisations.BaseInvoice')
),
migrations.RemoveField(
model_name='facture',
name='id',
),
migrations.RemoveField(
model_name='facture',
name='date',
),
migrations.AlterField(
model_name='facture',
name='baseinvoice_ptr',
field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice'),
)
]

View file

@ -55,80 +55,11 @@ from cotisations.utils import find_payment_method
from cotisations.validators import check_no_balance from cotisations.validators import check_no_balance
# TODO : change facture to invoice class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
"""
The model for an invoice. It reprensents the fact that a user paid for
something (it can be multiple article paid at once).
An invoice is linked to :
* one or more purchases (one for each article sold that time)
* a user (the one who bought those articles)
* a payment method (the one used by the user)
* (if applicable) a bank
* (if applicable) a cheque number.
Every invoice is dated throught the 'date' value.
An invoice has a 'controlled' value (default : False) which means that
someone with high enough rights has controlled that invoice and taken it
into account. It also has a 'valid' value (default : True) which means
that someone with high enough rights has decided that this invoice was not
valid (thus it's like the user never paid for his articles). It may be
necessary in case of non-payment.
"""
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True
)
# TODO : maybe change to cheque nummber because not evident
cheque = models.CharField(
max_length=255,
blank=True,
verbose_name=_l("Cheque number")
)
date = models.DateTimeField( date = models.DateTimeField(
auto_now_add=True, auto_now_add=True,
verbose_name=_l("Date") verbose_name=_l("Date")
) )
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
verbose_name=_l("Validated")
)
# TODO : changed name to controlled for clarity
control = models.BooleanField(
default=False,
verbose_name=_l("Controlled")
)
class Meta:
abstract = False
permissions = (
# TODO : change facture to invoice
('change_facture_control',
_l("Can change the \"controlled\" state")),
# TODO : seems more likely to be call create_facture_pdf
# or create_invoice_pdf
('change_facture_pdf',
_l("Can create a custom PDF invoice")),
('view_facture',
_l("Can see an invoice's details")),
('change_all_facture',
_l("Can edit all the previous invoices")),
)
verbose_name = _l("Invoice")
verbose_name_plural = _l("Invoices")
def linked_objects(self):
"""Return linked objects : machine and domain.
Usefull in history display"""
return self.vente_set.all()
# TODO : change prix to price # TODO : change prix to price
def prix(self): def prix(self):
@ -167,6 +98,78 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
).values_list('name', flat=True)) ).values_list('name', flat=True))
return name return name
# TODO : change facture to invoice
class Facture(BaseInvoice):
"""
The model for an invoice. It reprensents the fact that a user paid for
something (it can be multiple article paid at once).
An invoice is linked to :
* one or more purchases (one for each article sold that time)
* a user (the one who bought those articles)
* a payment method (the one used by the user)
* (if applicable) a bank
* (if applicable) a cheque number.
Every invoice is dated throught the 'date' value.
An invoice has a 'controlled' value (default : False) which means that
someone with high enough rights has controlled that invoice and taken it
into account. It also has a 'valid' value (default : True) which means
that someone with high enough rights has decided that this invoice was not
valid (thus it's like the user never paid for his articles). It may be
necessary in case of non-payment.
"""
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True
)
# TODO : maybe change to cheque nummber because not evident
cheque = models.CharField(
max_length=255,
blank=True,
verbose_name=_l("Cheque number")
)
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
verbose_name=_l("Validated")
)
# TODO : changed name to controlled for clarity
control = models.BooleanField(
default=False,
verbose_name=_l("Controlled")
)
class Meta:
abstract = False
permissions = (
# TODO : change facture to invoice
('change_facture_control',
_l("Can change the \"controlled\" state")),
# TODO : seems more likely to be call create_facture_pdf
# or create_invoice_pdf
('change_facture_pdf',
_l("Can create a custom PDF invoice")),
('view_facture',
_l("Can see an invoice's details")),
('change_all_facture',
_l("Can edit all the previous invoices")),
)
verbose_name = _l("Invoice")
verbose_name_plural = _l("Invoices")
def linked_objects(self):
"""Return linked objects : machine and domain.
Usefull in history display"""
return self.vente_set.all()
def can_edit(self, user_request, *args, **kwargs): def can_edit(self, user_request, *args, **kwargs):
if not user_request.has_perm('cotisations.change_facture'): if not user_request.has_perm('cotisations.change_facture'):
return False, _("You don't have the right to edit an invoice.") return False, _("You don't have the right to edit an invoice.")
@ -265,6 +268,28 @@ def facture_post_delete(**kwargs):
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class CustomInvoice(BaseInvoice):
class Meta:
permissions = (
('view_custom_invoice', _l("Can view a custom invoice")),
)
recipient = models.CharField(
max_length=255,
verbose_name=_l("Recipient")
)
payment = models.CharField(
max_length=255,
verbose_name=_l("Payment type")
)
address = models.CharField(
max_length=255,
verbose_name=_l("Address")
)
paid = models.BooleanField(
verbose_name="Paid"
)
# TODO : change Vente to Purchase # TODO : change Vente to Purchase
class Vente(RevMixin, AclMixin, models.Model): class Vente(RevMixin, AclMixin, models.Model):
""" """
@ -288,7 +313,7 @@ class Vente(RevMixin, AclMixin, models.Model):
# TODO : change facture to invoice # TODO : change facture to invoice
facture = models.ForeignKey( facture = models.ForeignKey(
'Facture', 'BaseInvoice',
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_l("Invoice") verbose_name=_l("Invoice")
) )
@ -355,6 +380,10 @@ class Vente(RevMixin, AclMixin, models.Model):
cotisation_type defined (which means the article sold represents cotisation_type defined (which means the article sold represents
a cotisation) a cotisation)
""" """
try:
invoice = self.facture.facture
except Facture.DoesNotExist:
return
if not hasattr(self, 'cotisation') and self.type_cotisation: if not hasattr(self, 'cotisation') and self.type_cotisation:
cotisation = Cotisation(vente=self) cotisation = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation cotisation.type_cotisation = self.type_cotisation
@ -362,7 +391,7 @@ class Vente(RevMixin, AclMixin, models.Model):
end_cotisation = Cotisation.objects.filter( end_cotisation = Cotisation.objects.filter(
vente__in=Vente.objects.filter( vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter( facture__in=Facture.objects.filter(
user=self.facture.user user=invoice.user
).exclude(valid=False)) ).exclude(valid=False))
).filter( ).filter(
Q(type_cotisation='All') | Q(type_cotisation='All') |
@ -371,9 +400,9 @@ class Vente(RevMixin, AclMixin, models.Model):
date_start__lt=date_start date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max'] ).aggregate(Max('date_end'))['date_end__max']
elif self.type_cotisation == "Adhesion": elif self.type_cotisation == "Adhesion":
end_cotisation = self.facture.user.end_adhesion() end_cotisation = invoice.user.end_adhesion()
else: else:
end_cotisation = self.facture.user.end_connexion() end_cotisation = invoice.user.end_connexion()
date_start = date_start or timezone.now() date_start = date_start or timezone.now()
end_cotisation = end_cotisation or date_start end_cotisation = end_cotisation or date_start
date_max = max(end_cotisation, date_start) date_max = max(end_cotisation, date_start)
@ -445,6 +474,10 @@ def vente_post_save(**kwargs):
LDAP user when a purchase has been saved. LDAP user when a purchase has been saved.
""" """
purchase = kwargs['instance'] purchase = kwargs['instance']
try:
purchase.facture.facture
except Facture.DoesNotExist:
return
if hasattr(purchase, 'cotisation'): if hasattr(purchase, 'cotisation'):
purchase.cotisation.vente = purchase purchase.cotisation.vente = purchase
purchase.cotisation.save() purchase.cotisation.save()
@ -462,8 +495,12 @@ def vente_post_delete(**kwargs):
Synchronise the LDAP user after a purchase has been deleted. Synchronise the LDAP user after a purchase has been deleted.
""" """
purchase = kwargs['instance'] purchase = kwargs['instance']
try:
invoice = purchase.facture.facture
except Facture.DoesNotExist:
return
if purchase.type_cotisation: if purchase.type_cotisation:
user = purchase.facture.user user = invoice.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)

View file

@ -0,0 +1,108 @@
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
Copyright © 2018 Hugo Levy-Falk
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load i18n %}
{% load acl %}
<div class="table-responsive">
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>
{% trans "Recipient" as tr_recip %}
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %}
</th>
<th>{% trans "Designation" %}</th>
<th>{% trans "Total price" %}</th>
<th>
{% trans "Payment method" as tr_payment_method %}
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
</th>
<th>
{% trans "Date" as tr_date %}
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
</th>
<th>
{% trans "Invoice id" as tr_invoice_id %}
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_invoice_id %}
</th>
<th>{% trans "Paid" %}</th>
<th></th>
<th></th>
</tr>
</thead>
{% for invoice in custom_invoice_list %}
<tr>
<td>{{ invoice.recipient }}</td>
<td>{{ invoice.name }}</td>
<td>{{ invoice.prix_total }}</td>
<td>{{ invoice.payment }}</td>
<td>{{ invoice.date }}</td>
<td>{{ invoice.id }}</td>
<td>{{ invoice.paid }}</td>
<td>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="editinvoice" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{% trans "Edit" %}<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="editinvoice">
{% can_edit invoice %}
<li>
<a href="{% url 'cotisations:edit-custom-invoice' invoice.id %}">
<i class="fa fa-dollar-sign"></i> {% trans "Edit" %}
</a>
</li>
{% acl_end %}
{% can_delete invoice %}
<li>
<a href="{% url 'cotisations:del-custom-invoice' invoice.id %}">
<i class="fa fa-trash"></i> {% trans "Delete" %}
</a>
</li>
{% acl_end %}
<li>
<a href="{% url 'cotisations:history' 'custominvoice' invoice.id %}">
<i class="fa fa-history"></i> {% trans "Historique" %}
</a>
</li>
</ul>
</div>
</td>
<td>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:custom-invoice-pdf' invoice.id %}">
<i class="fa fa-file-pdf"></i> {% trans "PDF" %}
</a>
</td>
</tr>
{% endfor %}
</table>
{% if custom_invoice_list.paginator %}
{% include 'pagination.html' with list=custom_invoice_list %}
{% endif %}
</div>

View file

@ -0,0 +1,36 @@
{% extends "cotisations/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load acl %}
{% load i18n %}
{% block title %}{% trans "Custom invoices" %}{% endblock %}
{% block content %}
<h2>{% trans "Custom invoices list" %}</h2>
{% can_create CustomInvoice %}
{% include "buttons/add.html" with href='cotisations:new-custom-invoice'%}
{% acl_end %}
{% include 'cotisations/aff_custom_invoice.html' with custom_invoice_list=custom_invoice_list %}
{% endblock %}

View file

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block sidebar %} {% block sidebar %}
{% can_change Facture pdf %} {% can_change Facture pdf %}
<a class="list-group-item list-group-item-success" href="{% url "cotisations:new-facture-pdf" %}"> <a class="list-group-item list-group-item-success" href="{% url "cotisations:new-custom-invoice" %}">
<i class="fa fa-plus"></i> {% trans "Create an invoice" %} <i class="fa fa-plus"></i> {% trans "Create an invoice" %}
</a> </a>
<a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}"> <a class="list-group-item list-group-item-warning" href="{% url "cotisations:control" %}">
@ -40,6 +40,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="fa fa-list-ul"></i> {% trans "Invoices" %} <i class="fa fa-list-ul"></i> {% trans "Invoices" %}
</a> </a>
{% acl_end %} {% acl_end %}
{% can_view_all CustomInvoice %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-custom-invoice" %}">
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
</a>
{% acl_end %}
{% can_view_all Article %} {% can_view_all Article %}
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}"> <a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
<i class="fa fa-list-ul"></i> {% trans "Available articles" %} <i class="fa fa-list-ul"></i> {% trans "Available articles" %}

View file

@ -52,9 +52,29 @@ urlpatterns = [
name='facture-pdf' name='facture-pdf'
), ),
url( url(
r'^new_facture_pdf/$', r'^index_custom_invoice/$',
views.new_facture_pdf, views.index_custom_invoice,
name='new-facture-pdf' name='index-custom-invoice'
),
url(
r'^new_custom_invoice/$',
views.new_custom_invoice,
name='new-custom-invoice'
),
url(
r'^edit_custom_invoice/(?P<custominvoiceid>[0-9]+)$',
views.edit_custom_invoice,
name='edit-custom-invoice'
),
url(
r'^custom_invoice_pdf/(?P<custominvoiceid>[0-9]+)$',
views.custom_invoice_pdf,
name='custom-invoice-pdf',
),
url(
r'^del_custom_invoice/(?P<custominvoiceid>[0-9]+)$',
views.del_custom_invoice,
name='del-custom-invoice'
), ),
url( url(
r'^credit_solde/(?P<userid>[0-9]+)$', r'^credit_solde/(?P<userid>[0-9]+)$',

View file

@ -58,7 +58,15 @@ from re2o.acl import (
can_change, can_change,
) )
from preferences.models import AssoOption, GeneralOption from preferences.models import AssoOption, GeneralOption
from .models import Facture, Article, Vente, Paiement, Banque from .models import (
Facture,
Article,
Vente,
Paiement,
Banque,
CustomInvoice,
BaseInvoice
)
from .forms import ( from .forms import (
FactureForm, FactureForm,
ArticleForm, ArticleForm,
@ -67,10 +75,10 @@ from .forms import (
DelPaiementForm, DelPaiementForm,
BanqueForm, BanqueForm,
DelBanqueForm, DelBanqueForm,
NewFactureFormPdf,
SelectUserArticleForm, SelectUserArticleForm,
SelectClubArticleForm, SelectClubArticleForm,
RechargeForm RechargeForm,
CustomInvoiceForm
) )
from .tex import render_invoice from .tex import render_invoice
from .payment_methods.forms import payment_method_factory from .payment_methods.forms import payment_method_factory
@ -178,10 +186,10 @@ def new_facture(request, user, userid):
# TODO : change facture to invoice # TODO : change facture to invoice
@login_required @login_required
@can_change(Facture, 'pdf') @can_create(CustomInvoice)
def new_facture_pdf(request): def new_custom_invoice(request):
""" """
View used to generate a custom PDF invoice. It's mainly used to View used to generate a custom invoice. It's mainly used to
get invoices that are not taken into account, for the administrative get invoices that are not taken into account, for the administrative
point of view. point of view.
""" """
@ -190,7 +198,7 @@ def new_facture_pdf(request):
Q(type_user='All') | Q(type_user=request.user.class_name) Q(type_user='All') | Q(type_user=request.user.class_name)
) )
# Building the invocie form and the article formset # Building the invocie form and the article formset
invoice_form = NewFactureFormPdf(request.POST or None) invoice_form = CustomInvoiceForm(request.POST or None)
if request.user.is_class_club: if request.user.is_class_club:
articles_formset = formset_factory(SelectClubArticleForm)( articles_formset = formset_factory(SelectClubArticleForm)(
request.POST or None, request.POST or None,
@ -202,44 +210,31 @@ def new_facture_pdf(request):
form_kwargs={'user': request.user} form_kwargs={'user': request.user}
) )
if invoice_form.is_valid() and articles_formset.is_valid(): if invoice_form.is_valid() and articles_formset.is_valid():
# Get the article list and build an list out of it new_invoice_instance = invoice_form.save()
# contiaining (article_name, article_price, quantity, total_price) for art_item in articles_formset:
articles_info = [] if art_item.cleaned_data:
for articles_form in articles_formset: article = art_item.cleaned_data['article']
if articles_form.cleaned_data: quantity = art_item.cleaned_data['quantity']
article = articles_form.cleaned_data['article'] Vente.objects.create(
quantity = articles_form.cleaned_data['quantity'] facture=new_invoice_instance,
articles_info.append({ name=article.name,
'name': article.name, prix=article.prix,
'price': article.prix, type_cotisation=article.type_cotisation,
'quantity': quantity, duration=article.duration,
'total_price': article.prix * quantity number=quantity
}) )
paid = invoice_form.cleaned_data['paid'] messages.success(
recipient = invoice_form.cleaned_data['dest'] request,
address = invoice_form.cleaned_data['chambre'] _('The custom invoice was successfully created.')
total_price = sum(a['total_price'] for a in articles_info) )
return redirect(reverse('cotisations:index-custom-invoice'))
return render_invoice(request, {
'DATE': timezone.now(),
'recipient_name': recipient,
'address': address,
'article': articles_info,
'total': total_price,
'paid': paid,
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
return form({ return form({
'factureform': invoice_form, 'factureform': invoice_form,
'action_name': _("Create"), 'action_name': _("Create"),
'articlesformset': articles_formset, 'articlesformset': articles_formset,
'articles': articles 'articlelist': articles
}, 'cotisations/facture.html', request) }, 'cotisations/facture.html', request)
@ -292,7 +287,7 @@ def facture_pdf(request, facture, **_kwargs):
def edit_facture(request, facture, **_kwargs): def edit_facture(request, facture, **_kwargs):
""" """
View used to edit an existing invoice. View used to edit an existing invoice.
Articles can be added or remove to the invoice and quantity Articles can be added or removed to the invoice and quantity
can be set as desired. This is also the view used to invalidate can be set as desired. This is also the view used to invalidate
an invoice. an invoice.
""" """
@ -347,6 +342,100 @@ def del_facture(request, facture, **_kwargs):
}, 'cotisations/delete.html', request) }, 'cotisations/delete.html', request)
@login_required
@can_edit(CustomInvoice)
def edit_custom_invoice(request, invoice, **kwargs):
# Building the invocie form and the article formset
invoice_form = CustomInvoiceForm(
request.POST or None,
instance=invoice
)
purchases_objects = Vente.objects.filter(facture=invoice)
purchase_form_set = modelformset_factory(
Vente,
fields=('name', 'number'),
extra=0,
max_num=len(purchases_objects)
)
purchase_form = purchase_form_set(
request.POST or None,
queryset=purchases_objects
)
if invoice_form.is_valid() and purchase_form.is_valid():
if invoice_form.changed_data:
invoice_form.save()
purchase_form.save()
messages.success(
request,
_("The invoice has been successfully edited.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
return form({
'factureform': invoice_form,
'venteform': purchase_form
}, 'cotisations/edit_facture.html', request)
@login_required
@can_view(CustomInvoice)
def custom_invoice_pdf(request, invoice, **_kwargs):
"""
View used to generate a PDF file from an existing invoice in database
Creates a line for each Purchase (thus article sold) and generate the
invoice with the total price, the payment method, the address and the
legal information for the user.
"""
# TODO : change vente to purchase
purchases_objects = Vente.objects.all().filter(facture=invoice)
# Get the article list and build an list out of it
# contiaining (article_name, article_price, quantity, total_price)
purchases_info = []
for purchase in purchases_objects:
purchases_info.append({
'name': purchase.name,
'price': purchase.prix,
'quantity': purchase.number,
'total_price': purchase.prix_total
})
return render_invoice(request, {
'paid': invoice.paid,
'fid': invoice.id,
'DATE': invoice.date,
'recipient_name': invoice.recipient,
'address': invoice.address,
'article': purchases_info,
'total': invoice.prix_total(),
'asso_name': AssoOption.get_cached_value('name'),
'line1': AssoOption.get_cached_value('adresse1'),
'line2': AssoOption.get_cached_value('adresse2'),
'siret': AssoOption.get_cached_value('siret'),
'email': AssoOption.get_cached_value('contact'),
'phone': AssoOption.get_cached_value('telephone'),
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
})
# TODO : change facture to invoice
@login_required
@can_delete(CustomInvoice)
def del_custom_invoice(request, invoice, **_kwargs):
"""
View used to delete an existing invocie.
"""
if request.method == "POST":
invoice.delete()
messages.success(
request,
_("The invoice has been successfully deleted.")
)
return redirect(reverse('cotisations:index-custom-invoice'))
return form({
'objet': invoice,
'objet_name': _("Invoice")
}, 'cotisations/delete.html', request)
@login_required @login_required
@can_create(Article) @can_create(Article)
def add_article(request): def add_article(request):
@ -681,8 +770,31 @@ def index_banque(request):
}) })
@login_required
@can_view_all(CustomInvoice)
def index_custom_invoice(request):
"""View used to display every custom invoice."""
pagination_number = GeneralOption.get_cached_value('pagination_number')
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
custom_invoice_list = SortTable.sort(
custom_invoice_list,
request.GET.get('col'),
request.GET.get('order'),
SortTable.COTISATIONS_CUSTOM
)
custom_invoice_list = re2o_paginator(
request,
custom_invoice_list,
pagination_number,
)
return render(request, 'cotisations/index_custom_invoice.html', {
'custom_invoice_list': custom_invoice_list
})
@login_required @login_required
@can_view_all(Facture) @can_view_all(Facture)
@can_view_all(CustomInvoice)
def index(request): def index(request):
""" """
View used to display the list of all exisitng invoices. View used to display the list of all exisitng invoices.
@ -698,7 +810,7 @@ def index(request):
) )
invoice_list = re2o_paginator(request, invoice_list, pagination_number) invoice_list = re2o_paginator(request, invoice_list, pagination_number)
return render(request, 'cotisations/index.html', { return render(request, 'cotisations/index.html', {
'facture_list': invoice_list 'facture_list': invoice_list,
}) })

View file

@ -250,6 +250,14 @@ class SortTable:
'cotis_id': ['id'], 'cotis_id': ['id'],
'default': ['-date'] 'default': ['-date']
} }
COTISATIONS_CUSTOM = {
'invoice_date': ['date'],
'invoice_id': ['id'],
'invoice_recipient': ['recipient'],
'invoice_address': ['address'],
'invoice_payment': ['payment'],
'default': ['-date']
}
COTISATIONS_CONTROL = { COTISATIONS_CONTROL = {
'control_name': ['user__adherent__name'], 'control_name': ['user__adherent__name'],
'control_surname': ['user__surname'], 'control_surname': ['user__surname'],

View file

@ -21,6 +21,6 @@ You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc., with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %} {% endcomment %}
<a class="btn btn-primary btn-sm" role="button" href="{% url href id %}" title="{{ desc|default:"Ajouter" }}"> <a class="btn btn-primary btn-sm" role="button" href="{% if id %}{% url href id %}{% else %}{% url href %}{% endif %}" title="{{ desc|default:"Ajouter" }}">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
</a> </a>