|
@@ -9,6 +9,7 @@ from decimal import Decimal
|
|
|
|
|
|
from django.conf import settings
|
|
|
from django.db import models, transaction
|
|
|
+from django.utils import timezone
|
|
|
from django.utils.encoding import python_2_unicode_compatible
|
|
|
|
|
|
|
|
@@ -16,7 +17,7 @@ from coin.offers.models import OfferSubscription
|
|
|
from coin.members.models import Member
|
|
|
from coin.html2pdf import render_as_pdf
|
|
|
from coin.utils import private_files_storage, start_of_month, end_of_month, \
|
|
|
- postgresql_regexp
|
|
|
+ postgresql_regexp, send_templated_email
|
|
|
from coin.isp_database.context_processors import branding
|
|
|
|
|
|
accounting_log = logging.getLogger("coin.billing")
|
|
@@ -106,6 +107,7 @@ class InvoiceQuerySet(models.QuerySet):
|
|
|
return self.filter(number__regex=postgresql_regexp(
|
|
|
InvoiceNumber.RE_INVOICE_NUMBER))
|
|
|
|
|
|
+
|
|
|
class Invoice(models.Model):
|
|
|
|
|
|
INVOICES_STATUS_CHOICES = (
|
|
@@ -144,6 +146,10 @@ class Invoice(models.Model):
|
|
|
default=0,
|
|
|
verbose_name='montant payé')
|
|
|
|
|
|
+ date_last_reminder_email = models.DateTimeField(null=True, blank=True,
|
|
|
+ verbose_name="Date du dernier email de relance envoyé")
|
|
|
+
|
|
|
+
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
|
|
# First save to get a PK
|
|
@@ -245,6 +251,69 @@ class Invoice(models.Model):
|
|
|
def __unicode__(self):
|
|
|
return '#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)
|
|
|
|
|
|
+ def reminder_needed(self):
|
|
|
+
|
|
|
+ # If there's no member, there's nobody to be reminded
|
|
|
+ if self.member is None:
|
|
|
+ return False
|
|
|
+
|
|
|
+ # If bill is close or not validated yet, nope
|
|
|
+ if self.status != 'open' or not self.validated:
|
|
|
+ return False
|
|
|
+
|
|
|
+ from dateutil.relativedelta import relativedelta
|
|
|
+
|
|
|
+ # If bill is not at least one month old, nope
|
|
|
+ if self.date_due >= timezone.now()+relativedelta(weeks=-4):
|
|
|
+ return False
|
|
|
+
|
|
|
+ # If a reminder has been recently sent, nope
|
|
|
+ if (self.date_last_reminder_email
|
|
|
+ and (self.date_last_reminder_email
|
|
|
+ >= timezone.now() + relativedelta(weeks=-3))):
|
|
|
+ return False
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+ def send_reminder(self, auto=False):
|
|
|
+ """ Envoie un courrier pour rappeler à un abonné qu'une facture est
|
|
|
+ en attente de paiement
|
|
|
+
|
|
|
+ :param bill: id of the bill to remind
|
|
|
+ :param auto: is it an auto email? (changes slightly template content)
|
|
|
+ """
|
|
|
+
|
|
|
+ if not self.reminder_needed():
|
|
|
+ return False
|
|
|
+
|
|
|
+ accounting_log.info("Sending reminder email to %s to pay invoice %s"
|
|
|
+ % (str(self.member), str(self.number)))
|
|
|
+
|
|
|
+ from coin.isp_database.models import ISPInfo
|
|
|
+
|
|
|
+ isp_info = ISPInfo.objects.first()
|
|
|
+ kwargs = {}
|
|
|
+ # Il peut ne pas y avir d'ISPInfo, ou bien pas d'administrative_email
|
|
|
+ if isp_info and isp_info.administrative_email:
|
|
|
+ kwargs['from_email'] = isp_info.administrative_email
|
|
|
+
|
|
|
+ # Si le dernier courriel de relance a été envoyé il y a moins de trois
|
|
|
+ # semaines, n'envoi pas un nouveau courriel
|
|
|
+ send_templated_email(
|
|
|
+ to=self.member.email,
|
|
|
+ subject_template='billing/emails/reminder_for_unpaid_bill.txt',
|
|
|
+ body_template='billing/emails/reminder_for_unpaid_bill.html',
|
|
|
+ context={'member': self.member, 'branding': isp_info,
|
|
|
+ 'membership_info_url': settings.MEMBER_MEMBERSHIP_INFO_URL,
|
|
|
+ 'today': datetime.date.today,
|
|
|
+ 'auto_sent': auto},
|
|
|
+ **kwargs)
|
|
|
+
|
|
|
+ # Sauvegarde en base la date du dernier envoi de mail de relance
|
|
|
+ self.date_last_call_for_membership_fees_email = timezone.now()
|
|
|
+ self.save()
|
|
|
+ return True
|
|
|
+
|
|
|
class Meta:
|
|
|
verbose_name = 'facture'
|
|
|
|
|
@@ -330,7 +399,7 @@ class Payment(models.Model):
|
|
|
|
|
|
# If this payment is related to a member, update the accounting for
|
|
|
# this member
|
|
|
- if self.member != None:
|
|
|
+ if self.member is not None:
|
|
|
update_accounting_for_member(self.member)
|
|
|
|
|
|
def amount_not_allocated(self):
|