Browse Source

First working code to have a validation process of an invoice.
When invoice is created, it is in "draft" mode (note validated).
When staff pass invoice in validated status, it generate a pdf file of the invoice and store it in /media/invoices and link this pdf with invoice.
Currently, pdf can be downloaded by accessing pdf url in MEDIA_ROOT. This is not really secure because it means all invoices are publicly available.
For minium security, pdf filename are obfusced with random uuid.

Fabs 10 years ago
parent
commit
bc49726e36

+ 1 - 0
.gitignore

@@ -8,5 +8,6 @@ coin/settings_local.py
 .pydevproject
 .settings/*
 /static
+/media
 .idea
 venv

+ 26 - 3
coin/billing/admin.py

@@ -1,8 +1,13 @@
 # -*- coding: utf-8 -*-
 from django import forms
 from django.contrib import admin
+from django.contrib import messages
+from django.http import HttpResponseRedirect
+from django.conf.urls import url
+
 from coin.filtering_queryset import LimitedAdminInlineMixin
 from coin.billing.models import Invoice, InvoiceDetail, Payment
+from coin.billing.utils import get_invoice_from_id_or_number
 from coin.offers.models import OfferSubscription
 import autocomplete_light
 
@@ -37,14 +42,15 @@ class PaymentInline(admin.StackedInline):
 
 
 class InvoiceAdmin(admin.ModelAdmin):
-    list_display = ('number', 'date', 'status', 'amount', 'member')
+    list_display = ('number', 'date', 'status', 'amount', 'member', 'validated')
     list_display_links = ('number', 'date')
     inlines = [InvoiceDetailInline, PaymentInline]
     fields = (('number', 'date', 'status'),
        ('date_due'),
        ('member'),
-       ('amount','amount_paid'))
-    readonly_fields = ('amount','amount_paid')
+       ('amount','amount_paid'),
+       ('validated', 'pdf'))
+    readonly_fields = ('amount','amount_paid','validated','pdf')
     form = autocomplete_light.modelform_factory(Invoice)
 
     def get_formsets(self, request, obj=None):
@@ -62,4 +68,21 @@ class InvoiceAdmin(admin.ModelAdmin):
         else:
             pass
 
+    def get_urls(self):
+        urls = super(InvoiceAdmin, self).get_urls()
+        my_urls = [
+            url(r'^validate/(?P<id>.+)$', self.admin_site.admin_view(self.validate_view), name='invoice_validate'),
+        ]
+        return my_urls + urls
+
+    def validate_view(self, request, id):
+        if request.user.is_superuser:
+            invoice = get_invoice_from_id_or_number(id)
+            invoice.validate()
+            messages.success(request, 'La facture a été validée.')
+        else:
+            messages.error(request, 'Vous n\'avez pas l\'autorisation de valider une facture.')
+
+        return HttpResponseRedirect(request.META["HTTP_REFERER"])
+
 admin.site.register(Invoice, InvoiceAdmin)

+ 26 - 0
coin/billing/migrations/0003_auto_20140920_2342.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('billing', '0002_auto_20140919_2158'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='invoice',
+            name='pdf',
+            field=models.FileField(upload_to=b'invoices', null=True, verbose_name='Facture en PDF', blank=True),
+            preserve_default=True,
+        ),
+        migrations.AddField(
+            model_name='invoice',
+            name='validated',
+            field=models.BooleanField(default=False, verbose_name=b'Facture valid\xc3\xa9e'),
+            preserve_default=True,
+        ),
+    ]

+ 28 - 4
coin/billing/models.py

@@ -1,12 +1,16 @@
 # -*- coding: utf-8 -*-
 import datetime
 import random
+import uuid
 from decimal import Decimal
+
 from django.db import models
 from django.db.models.signals import post_save
 from django.dispatch import receiver
+
 from coin.offers.models import OfferSubscription
 from coin.members.models import Member
+from coin.html2pdf import render_as_pdf
 
 
 def next_invoice_number():
@@ -16,22 +20,21 @@ def next_invoice_number():
                               random.randrange(100, 999),
                               random.randrange(100, 999))
 
-
 class Invoice(models.Model):
 
     INVOICES_STATUS_CHOICES = (
-        ('draft', u'Brouillon'),
         ('open', u'A payer'),
         ('closed', u'Reglée'),
         ('trouble', u'Litige')
     )
 
+    validated = models.BooleanField(default=False, verbose_name='Facture validée')
     number = models.CharField(max_length=25,
                               default=next_invoice_number,
                               unique=True,
                               verbose_name='Numéro')
     status = models.CharField(max_length=50, choices=INVOICES_STATUS_CHOICES,
-                              default='draft',
+                              default='open',
                               verbose_name='Statut')
     date = models.DateField(default=datetime.date.today, null=True)
     date_due = models.DateField(
@@ -44,6 +47,9 @@ class Invoice(models.Model):
                                related_name='invoices',
                                verbose_name='Membre',
                                on_delete=models.SET_NULL)
+    pdf = models.FileField(upload_to='invoices',
+                           null=True, blank=True,
+                           verbose_name=u'Facture en PDF')
 
     def amount(self):
         "Calcul le montant de la facture en fonction des éléments de détails"
@@ -73,7 +79,25 @@ class Invoice(models.Model):
 
     def has_owner(self, username):
         "Check if passed username (ex gmajax) is owner of the invoice"
-        return (self.username == username)
+        return (self.member.username == username)
+
+    def generate_pdf(self):
+        "Make and store a pdf file for the invoice"
+        pdf_file = render_as_pdf('billing/invoice_pdf.html', {"invoice": self})
+        self.pdf.save(u'%s_%s.pdf' % (self.number, uuid.uuid4()), pdf_file)
+
+    def validate(self):
+        """
+        Switch invoice to validate mode. This set to False the draft field
+        and generate the pdf
+        """
+        # if not self.is_validated():
+        self.validated = True
+        self.save()
+        self.generate_pdf()
+
+    def is_validated(self):
+        return self.validated and bool(self.pdf)
 
     def __unicode__(self):
         return u'#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)

+ 2 - 0
coin/billing/urls.py

@@ -6,5 +6,7 @@ urlpatterns = patterns(
     '',
     url(r'^invoice/(?P<id>.+).pdf$', views.invoice_pdf, name="invoice_pdf"),
     url(r'^invoice/(?P<id>.+)$', views.invoice, name="invoice"),
+    # url(r'^invoice/(?P<id>.+)/validate$', views.invoice_validate, name="invoice_validate"),
+    
     url('invoice/create_all_members_invoices_for_a_period', views.gen_invoices)
 )

+ 24 - 7
coin/billing/views.py

@@ -1,8 +1,10 @@
 # -*- coding: utf-8 -*-
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseRedirect
 from django.template import RequestContext
 from django.shortcuts import render, render_to_response
 from django.core.exceptions import PermissionDenied
+from django.contrib import messages
+
 from coin.billing.models import Invoice
 from coin.members.models import Member
 from coin.html2pdf import render_as_pdf
@@ -18,21 +20,36 @@ def invoice_pdf(request, id):
     Renvoi une facture générée en format pdf
     id peut être soit la pk d'une facture, soit le numero de facture
     """
-
     invoice = get_invoice_from_id_or_number(id)
 
     if not invoice.has_owner(request.user.username)\
        and not request.user.is_superuser:
         raise PermissionDenied
 
-    pdf = render_as_pdf('billing/invoice_pdf.html', {"invoice": invoice})
+    return HttpResponseRedirect(invoice.pdf.url)
 
-    response = HttpResponse(content_type='application/pdf')
-    #response['Content-Disposition'] = 'attachment; filename="facture.pdf"'
+    # pdf = render_as_pdf('billing/invoice_pdf.html', {"invoice": invoice})
 
-    response.write(pdf)
+    # response = HttpResponse(content_type='application/pdf')
+    # response['Content-Disposition'] = 'attachment; filename="facture.pdf"'
 
-    return response
+    # response.write(invoice.pdf)
+
+    # return response
+
+# def invoice_validate(request, id):
+#     """
+#     Valide la facture
+#     """
+#     #TODO change this by perm : has_validate_invoice_permission
+#     if request.user.is_superuser:
+#         invoice = get_invoice_from_id_or_number(id)
+#         invoice.validate()
+#         messages.success(request, 'La facture a été validée.')
+#     else:
+#         messages.error(request, 'Vous n\'avez pas l\'autorisation de valider une facture.')
+
+#     return HttpResponseRedirect(request.META["HTTP_REFERER"])
 
 def invoice(request, id):
     """

+ 8 - 8
coin/html2pdf.py

@@ -1,10 +1,12 @@
 # -*- coding: utf-8 -*-
 import os
 import re
-from django.conf import settings
-from tempfile import NamedTemporaryFile
 from xhtml2pdf import pisa
+from tempfile import NamedTemporaryFile
+
+from django.conf import settings
 from django.template import loader, Context
+from django.core.files import File
 
 
 def link_callback(uri, rel):
@@ -42,16 +44,14 @@ def link_callback(uri, rel):
 def render_as_pdf(template, context):
     """
     Génére le template indiqué avec les données du context en HTML et le
-    converti en PDF via le module xhtml2pdf
+    converti en PDF via le module xhtml2pdf.
+    Renvoi un objet de type File
     """
     template = loader.get_template(template)
     html = template.render(Context(context))
     file = NamedTemporaryFile()
 
     pisaStatus = pisa.CreatePDF(html, dest=file, link_callback=link_callback)
+    file.flush()
 
-    file.seek(0)
-    pdf = file.read()
-    file.close()
-
-    return pdf
+    return File(open(file.name))

+ 5 - 1
coin/templates/admin/billing/invoice/change_form.html

@@ -3,7 +3,11 @@
 {% block object-tools %}
 {% if change %}{% if not is_popup %}
 <ul class="object-tools">
-   <li><a href="{% url 'billing:invoice_pdf' id=object_id %}">Télécharger en PDF</a></li>
+    {% if not original.is_validated %}
+        <li><a href="{% url 'admin:invoice_validate' id=object_id %}">Valider la facture</a></li>
+    {% else %}
+        <li><a href="{% url 'billing:invoice_pdf' id=object_id %}">Télécharger le PDF</a></li>
+    {% endif %}
 </ul>
 {% endif %}{% endif %}
 {% endblock %}

+ 4 - 0
coin/urls.py

@@ -1,4 +1,6 @@
+from django.conf import settings
 from django.conf.urls import patterns, include, url
+from django.conf.urls.static import static
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
 import autocomplete_light
@@ -29,3 +31,5 @@ urlpatterns = patterns(
 )
 
 urlpatterns += staticfiles_urlpatterns()
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+