models.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. # -*- coding: utf-8 -*-
  2. import datetime
  3. import random
  4. import uuid
  5. import os
  6. from decimal import Decimal
  7. from django.db import models
  8. from django.db.models.signals import post_save
  9. from django.dispatch import receiver
  10. from coin.offers.models import OfferSubscription
  11. from coin.members.models import Member
  12. from coin.html2pdf import render_as_pdf
  13. from coin.utils import private_files_storage
  14. def next_invoice_number():
  15. "Détermine un numéro de facture aléatoire"
  16. return u'%s%02i-%i-%i' % (datetime.date.today().year,
  17. datetime.date.today().month,
  18. random.randrange(100, 999),
  19. random.randrange(100, 999))
  20. def invoice_pdf_filename(instance, filename):
  21. "Nom du fichier pdf à stocker pour les factures"
  22. return u'%d_%s_%s.pdf' % (instance.member.id, instance.number, uuid.uuid4())
  23. class Invoice(models.Model):
  24. INVOICES_STATUS_CHOICES = (
  25. ('open', u'A payer'),
  26. ('closed', u'Reglée'),
  27. ('trouble', u'Litige')
  28. )
  29. validated = models.BooleanField(default=False, verbose_name='Validée')
  30. number = models.CharField(max_length=25,
  31. default=next_invoice_number,
  32. unique=True,
  33. verbose_name='Numéro')
  34. status = models.CharField(max_length=50, choices=INVOICES_STATUS_CHOICES,
  35. default='open',
  36. verbose_name='Statut')
  37. date = models.DateField(default=datetime.date.today, null=True)
  38. date_due = models.DateField(
  39. default=(datetime.date(datetime.date.today().year,
  40. datetime.date.today().month + 1, 1) -
  41. datetime.timedelta(days=1)),
  42. null=True,
  43. verbose_name=u'Date d\'échéance de paiement')
  44. member = models.ForeignKey(Member, null=True, blank=True, default=None,
  45. related_name='invoices',
  46. verbose_name='Membre',
  47. on_delete=models.SET_NULL)
  48. pdf = models.FileField(storage=private_files_storage, upload_to=invoice_pdf_filename,
  49. null=True, blank=True,
  50. verbose_name=u'PDF')
  51. def amount(self):
  52. "Calcul le montant de la facture en fonction des éléments de détails"
  53. total = Decimal('0.0')
  54. for detail in self.details.all():
  55. total += detail.total()
  56. return total.quantize(Decimal('0.01'))
  57. amount.short_description = 'Montant'
  58. def amount_paid(self):
  59. """
  60. Calcul le montant payé de la facture en fonction des éléments
  61. de paiements
  62. """
  63. total = Decimal('0.0')
  64. for payment in self.payments.all():
  65. total += payment.amount
  66. return total.quantize(Decimal('0.01'))
  67. amount_paid.short_description = 'Montant payé'
  68. def amount_remaining_to_pay(self):
  69. """
  70. Calcul le montant restant à payer
  71. """
  72. return self.amount() - self.amount_paid()
  73. amount_remaining_to_pay.short_description = 'Reste à payer'
  74. def has_owner(self, username):
  75. "Check if passed username (ex gmajax) is owner of the invoice"
  76. return (self.member.username == username)
  77. def generate_pdf(self):
  78. "Make and store a pdf file for the invoice"
  79. pdf_file = render_as_pdf('billing/invoice_pdf.html', {"invoice": self})
  80. self.pdf.save(u'%s.pdf' % self.number, pdf_file)
  81. def validate(self):
  82. """
  83. Switch invoice to validate mode. This set to False the draft field
  84. and generate the pdf
  85. """
  86. if not self.is_validated():
  87. self.validated = True
  88. self.save()
  89. self.generate_pdf()
  90. def is_validated(self):
  91. return self.validated and bool(self.pdf) and private_files_storage.exists(self.pdf.name)
  92. def __unicode__(self):
  93. return u'#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)
  94. class Meta:
  95. verbose_name = 'facture'
  96. class InvoiceDetail(models.Model):
  97. label = models.CharField(max_length=100)
  98. amount = models.DecimalField(max_digits=5, decimal_places=2,
  99. verbose_name='Montant')
  100. quantity = models.DecimalField(null=True, verbose_name=u'Quantité',
  101. default=1.0, decimal_places=2, max_digits=4)
  102. tax = models.DecimalField(null=True, default=0.0, decimal_places=2,
  103. max_digits=4, verbose_name='TVA',
  104. help_text='en %')
  105. invoice = models.ForeignKey(Invoice, verbose_name='Facture',
  106. related_name='details')
  107. offersubscription = models.ForeignKey(OfferSubscription, null=True,
  108. blank=True, default=None,
  109. verbose_name='Abonnement')
  110. period_from = models.DateField(
  111. default=datetime.date(datetime.date.today().year,
  112. datetime.date.today().month, 1),
  113. null=True,
  114. blank=True,
  115. verbose_name=u'Début de période',
  116. help_text=u'Date de début de période sur laquelle est facturé cet item')
  117. period_to = models.DateField(
  118. default=(datetime.date(datetime.date.today().year,
  119. datetime.date.today().month + 1, 1) -
  120. datetime.timedelta(days=1)),
  121. null=True,
  122. blank=True,
  123. verbose_name=u'Fin de période',
  124. help_text=u'Date de fin de période sur laquelle est facturé cet item')
  125. def __unicode__(self):
  126. return self.label
  127. def total(self):
  128. "Calcul le total"
  129. return (self.amount * (self.tax / Decimal('100.0') +
  130. Decimal('1.0')) * self.quantity).quantize(Decimal('0.01'))
  131. class Meta:
  132. verbose_name = 'détail de facture'
  133. class Payment(models.Model):
  134. PAYMENT_MEAN_CHOICES = (
  135. ('cash', u'Espèces'),
  136. ('check', u'Chèque'),
  137. ('transfer', u'Virement'),
  138. ('other', u'Autre')
  139. )
  140. payment_mean = models.CharField(max_length=100, null=True,
  141. default='transfer',
  142. choices=PAYMENT_MEAN_CHOICES,
  143. verbose_name='Moyen de paiement')
  144. amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
  145. verbose_name='Montant')
  146. date = models.DateField(default=datetime.date.today)
  147. invoice = models.ForeignKey(Invoice, verbose_name='Facture',
  148. related_name='payments')
  149. def __unicode__(self):
  150. return u'Paiment de %0.2f€' % self.amount
  151. class Meta:
  152. verbose_name = 'paiement'
  153. @receiver(post_save, sender=Payment)
  154. def set_invoice_as_paid_if_needed(sender, instance, **kwargs):
  155. """
  156. Lorsqu'un paiement est enregistré, vérifie si la facture est alors
  157. complétement payée. Dans ce cas elle passe en réglée
  158. """
  159. if (instance.invoice.amount_paid >= instance.invoice.amount and
  160. instance.invoice.status == 'open'):
  161. instance.invoice.status = 'closed'
  162. instance.invoice.save()