models.py 6.9 KB

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