Parcourir la 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 il y a 10 ans
Parent
commit
bc49726e36

+ 1 - 0
.gitignore

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

+ 26 - 3
coin/billing/admin.py

@@ -1,8 +1,13 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from django import forms
 from django import forms
 from django.contrib import admin
 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.filtering_queryset import LimitedAdminInlineMixin
 from coin.billing.models import Invoice, InvoiceDetail, Payment
 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
 from coin.offers.models import OfferSubscription
 import autocomplete_light
 import autocomplete_light
 
 
@@ -37,14 +42,15 @@ class PaymentInline(admin.StackedInline):
 
 
 
 
 class InvoiceAdmin(admin.ModelAdmin):
 class InvoiceAdmin(admin.ModelAdmin):
-    list_display = ('number', 'date', 'status', 'amount', 'member')
+    list_display = ('number', 'date', 'status', 'amount', 'member', 'validated')
     list_display_links = ('number', 'date')
     list_display_links = ('number', 'date')
     inlines = [InvoiceDetailInline, PaymentInline]
     inlines = [InvoiceDetailInline, PaymentInline]
     fields = (('number', 'date', 'status'),
     fields = (('number', 'date', 'status'),
        ('date_due'),
        ('date_due'),
        ('member'),
        ('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)
     form = autocomplete_light.modelform_factory(Invoice)
 
 
     def get_formsets(self, request, obj=None):
     def get_formsets(self, request, obj=None):
@@ -62,4 +68,21 @@ class InvoiceAdmin(admin.ModelAdmin):
         else:
         else:
             pass
             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)
 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 -*-
 # -*- coding: utf-8 -*-
 import datetime
 import datetime
 import random
 import random
+import uuid
 from decimal import Decimal
 from decimal import Decimal
+
 from django.db import models
 from django.db import models
 from django.db.models.signals import post_save
 from django.db.models.signals import post_save
 from django.dispatch import receiver
 from django.dispatch import receiver
+
 from coin.offers.models import OfferSubscription
 from coin.offers.models import OfferSubscription
 from coin.members.models import Member
 from coin.members.models import Member
+from coin.html2pdf import render_as_pdf
 
 
 
 
 def next_invoice_number():
 def next_invoice_number():
@@ -16,22 +20,21 @@ def next_invoice_number():
                               random.randrange(100, 999),
                               random.randrange(100, 999),
                               random.randrange(100, 999))
                               random.randrange(100, 999))
 
 
-
 class Invoice(models.Model):
 class Invoice(models.Model):
 
 
     INVOICES_STATUS_CHOICES = (
     INVOICES_STATUS_CHOICES = (
-        ('draft', u'Brouillon'),
         ('open', u'A payer'),
         ('open', u'A payer'),
         ('closed', u'Reglée'),
         ('closed', u'Reglée'),
         ('trouble', u'Litige')
         ('trouble', u'Litige')
     )
     )
 
 
+    validated = models.BooleanField(default=False, verbose_name='Facture validée')
     number = models.CharField(max_length=25,
     number = models.CharField(max_length=25,
                               default=next_invoice_number,
                               default=next_invoice_number,
                               unique=True,
                               unique=True,
                               verbose_name='Numéro')
                               verbose_name='Numéro')
     status = models.CharField(max_length=50, choices=INVOICES_STATUS_CHOICES,
     status = models.CharField(max_length=50, choices=INVOICES_STATUS_CHOICES,
-                              default='draft',
+                              default='open',
                               verbose_name='Statut')
                               verbose_name='Statut')
     date = models.DateField(default=datetime.date.today, null=True)
     date = models.DateField(default=datetime.date.today, null=True)
     date_due = models.DateField(
     date_due = models.DateField(
@@ -44,6 +47,9 @@ class Invoice(models.Model):
                                related_name='invoices',
                                related_name='invoices',
                                verbose_name='Membre',
                                verbose_name='Membre',
                                on_delete=models.SET_NULL)
                                on_delete=models.SET_NULL)
+    pdf = models.FileField(upload_to='invoices',
+                           null=True, blank=True,
+                           verbose_name=u'Facture en PDF')
 
 
     def amount(self):
     def amount(self):
         "Calcul le montant de la facture en fonction des éléments de détails"
         "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):
     def has_owner(self, username):
         "Check if passed username (ex gmajax) is owner of the invoice"
         "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):
     def __unicode__(self):
         return u'#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)
         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>.+).pdf$', views.invoice_pdf, name="invoice_pdf"),
     url(r'^invoice/(?P<id>.+)$', views.invoice, name="invoice"),
     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)
     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 -*-
 # -*- coding: utf-8 -*-
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseRedirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.shortcuts import render, render_to_response
 from django.shortcuts import render, render_to_response
 from django.core.exceptions import PermissionDenied
 from django.core.exceptions import PermissionDenied
+from django.contrib import messages
+
 from coin.billing.models import Invoice
 from coin.billing.models import Invoice
 from coin.members.models import Member
 from coin.members.models import Member
 from coin.html2pdf import render_as_pdf
 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
     Renvoi une facture générée en format pdf
     id peut être soit la pk d'une facture, soit le numero de facture
     id peut être soit la pk d'une facture, soit le numero de facture
     """
     """
-
     invoice = get_invoice_from_id_or_number(id)
     invoice = get_invoice_from_id_or_number(id)
 
 
     if not invoice.has_owner(request.user.username)\
     if not invoice.has_owner(request.user.username)\
        and not request.user.is_superuser:
        and not request.user.is_superuser:
         raise PermissionDenied
         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):
 def invoice(request, id):
     """
     """

+ 8 - 8
coin/html2pdf.py

@@ -1,10 +1,12 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 import os
 import os
 import re
 import re
-from django.conf import settings
-from tempfile import NamedTemporaryFile
 from xhtml2pdf import pisa
 from xhtml2pdf import pisa
+from tempfile import NamedTemporaryFile
+
+from django.conf import settings
 from django.template import loader, Context
 from django.template import loader, Context
+from django.core.files import File
 
 
 
 
 def link_callback(uri, rel):
 def link_callback(uri, rel):
@@ -42,16 +44,14 @@ def link_callback(uri, rel):
 def render_as_pdf(template, context):
 def render_as_pdf(template, context):
     """
     """
     Génére le template indiqué avec les données du context en HTML et le
     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)
     template = loader.get_template(template)
     html = template.render(Context(context))
     html = template.render(Context(context))
     file = NamedTemporaryFile()
     file = NamedTemporaryFile()
 
 
     pisaStatus = pisa.CreatePDF(html, dest=file, link_callback=link_callback)
     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 %}
 {% block object-tools %}
 {% if change %}{% if not is_popup %}
 {% if change %}{% if not is_popup %}
 <ul class="object-tools">
 <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>
 </ul>
 {% endif %}{% endif %}
 {% endif %}{% endif %}
 {% endblock %}
 {% 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 import patterns, include, url
+from django.conf.urls.static import static
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
 
 import autocomplete_light
 import autocomplete_light
@@ -29,3 +31,5 @@ urlpatterns = patterns(
 )
 )
 
 
 urlpatterns += staticfiles_urlpatterns()
 urlpatterns += staticfiles_urlpatterns()
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+