models.py 7.9 KB

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