|
@@ -5,11 +5,13 @@ import datetime
|
|
|
import random
|
|
|
import uuid
|
|
|
import os
|
|
|
+import re
|
|
|
from decimal import Decimal
|
|
|
|
|
|
from django.db import models, transaction
|
|
|
from django.db.models.signals import post_save
|
|
|
from django.dispatch import receiver
|
|
|
+from django.utils.encoding import python_2_unicode_compatible
|
|
|
|
|
|
from coin.offers.models import OfferSubscription
|
|
|
from coin.members.models import Member
|
|
@@ -18,13 +20,6 @@ from coin.utils import private_files_storage, start_of_month, end_of_month, \
|
|
|
disable_for_loaddata
|
|
|
from coin.isp_database.context_processors import branding
|
|
|
|
|
|
-def next_invoice_number():
|
|
|
- """Détermine un numéro de facture aléatoire"""
|
|
|
- return '%s%02i-%i-%i' % (datetime.date.today().year,
|
|
|
- datetime.date.today().month,
|
|
|
- random.randrange(100, 999),
|
|
|
- random.randrange(100, 999))
|
|
|
-
|
|
|
|
|
|
def invoice_pdf_filename(instance, filename):
|
|
|
"""Nom et chemin du fichier pdf à stocker pour les factures"""
|
|
@@ -33,6 +28,66 @@ def invoice_pdf_filename(instance, filename):
|
|
|
instance.number,
|
|
|
uuid.uuid4())
|
|
|
|
|
|
+@python_2_unicode_compatible
|
|
|
+class InvoiceNumber:
|
|
|
+ """ Logic and validation of invoice numbers
|
|
|
+
|
|
|
+ Defines invoice numbers serie in a way that is legal in france.
|
|
|
+
|
|
|
+ https://www.service-public.fr/professionnels-entreprises/vosdroits/F23208#fiche-item-3
|
|
|
+
|
|
|
+ Our format is YYYY-MM-XXXXXX
|
|
|
+ - YYYY the year of the bill
|
|
|
+ - MM month of the bill
|
|
|
+ - XXXXXX a per-month sequence
|
|
|
+ """
|
|
|
+ RE_INVOICE_NUMBER = re.compile(
|
|
|
+ r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<index>\d{6})')
|
|
|
+
|
|
|
+ def __init__(self, date, index):
|
|
|
+ self.date = date
|
|
|
+ self.index = index
|
|
|
+
|
|
|
+ def get_next(self):
|
|
|
+ return InvoiceNumber(self.date, self.index + 1)
|
|
|
+
|
|
|
+ def __str__(self):
|
|
|
+ return '{:%Y-%m}-{:0>6}'.format(self.date, self.index)
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def parse(cls, string):
|
|
|
+ m = cls.RE_INVOICE_NUMBER.match(string)
|
|
|
+ if not m:
|
|
|
+ raise ValueError('Not a valid invoice number: "{}"'.format(string))
|
|
|
+
|
|
|
+ return cls(
|
|
|
+ datetime.date(
|
|
|
+ year=int(m.group('year')),
|
|
|
+ month=int(m.group('month')),
|
|
|
+ day=1),
|
|
|
+ int(m.group('index')))
|
|
|
+
|
|
|
+
|
|
|
+class InvoiceQuerySet(models.QuerySet):
|
|
|
+ def get_next_invoice_number(self, date):
|
|
|
+ last_invoice_number_str = self._get_last_invoice_number(date)
|
|
|
+
|
|
|
+ if last_invoice_number_str is None:
|
|
|
+ # It's the first bill of the month
|
|
|
+ invoice_number = InvoiceNumber(date, 1)
|
|
|
+ else:
|
|
|
+ invoice_number = InvoiceNumber.parse(last_invoice_number_str).get_next()
|
|
|
+
|
|
|
+ return str(invoice_number)
|
|
|
+
|
|
|
+ def _get_last_invoice_number(self, date):
|
|
|
+ return self.with_valid_number().aggregate(
|
|
|
+ models.Max('number'))['number__max']
|
|
|
+
|
|
|
+ def with_valid_number(self):
|
|
|
+ """ Excludes previous numbering schemes or draft invoices
|
|
|
+ """
|
|
|
+ return self.filter(number__regex=InvoiceNumber.RE_INVOICE_NUMBER.pattern)
|
|
|
|
|
|
class Invoice(models.Model):
|
|
|
|
|
@@ -46,7 +101,6 @@ class Invoice(models.Model):
|
|
|
help_text='Once validated, a PDF is generated'
|
|
|
' and the invoice cannot be modified')
|
|
|
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,
|
|
@@ -117,15 +171,12 @@ class Invoice(models.Model):
|
|
|
Switch invoice to validate mode. This set to False the draft field
|
|
|
and generate the pdf
|
|
|
"""
|
|
|
+ self.number = Invoice.objects.get_next_invoice_number(self.date)
|
|
|
self.validated = True
|
|
|
self.save()
|
|
|
self.generate_pdf()
|
|
|
assert self.pdf_exists()
|
|
|
|
|
|
- def save(self, *args, **kwargs):
|
|
|
- self.number = Invoice.objects.get_next_invoice_number(self.date)
|
|
|
- super(Invoice, self).save(*args, **kwargs)
|
|
|
-
|
|
|
def pdf_exists(self):
|
|
|
return (self.validated
|
|
|
and bool(self.pdf)
|
|
@@ -141,6 +192,7 @@ class Invoice(models.Model):
|
|
|
class Meta:
|
|
|
verbose_name = 'facture'
|
|
|
|
|
|
+ objects = InvoiceQuerySet().as_manager()
|
|
|
|
|
|
class InvoiceDetail(models.Model):
|
|
|
|