Parcourir la source

Adding logging for billing

Alexandre Aubin il y a 7 ans
Parent
commit
00d5d23e95

+ 2 - 2
coin/billing/management/commands/import_payments_from_csv.py

@@ -274,7 +274,7 @@ should run this command with --commit if you agree with the dry-run."""
             for knownPayment in knownPayments:
             for knownPayment in knownPayments:
 
 
                 if  (str(knownPayment.date) == str(payment["date"])) \
                 if  (str(knownPayment.date) == str(payment["date"])) \
-                and (str(knownPayment.label_from_bank) == str(payment["label"])) \
+                and (str(knownPayment.label) == str(payment["label"])) \
                 and (float(knownPayment.amount) == float(payment["amount"])):
                 and (float(knownPayment.amount) == float(payment["amount"])):
                     foundMatch = True
                     foundMatch = True
                     break
                     break
@@ -301,7 +301,7 @@ should run this command with --commit if you agree with the dry-run."""
             print newPayment
             print newPayment
             # Create the payment
             # Create the payment
             payment = Payment.objects.create(amount=float(newPayment["amount"]),
             payment = Payment.objects.create(amount=float(newPayment["amount"]),
-                                             label_from_bank=str(newPayment["label"]),
+                                             label=str(newPayment["label"]),
                                              date=str(newPayment["date"]),
                                              date=str(newPayment["date"]),
                                              member=member)
                                              member=member)
 
 

+ 73 - 44
coin/billing/models.py

@@ -2,16 +2,13 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import datetime
 import datetime
-import random
 import uuid
 import uuid
-import os
 import re
 import re
+import logging
 from decimal import Decimal
 from decimal import Decimal
 
 
 from django.conf import settings
 from django.conf import settings
 from django.db import models, transaction
 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 django.utils.encoding import python_2_unicode_compatible
 
 
 
 
@@ -19,9 +16,11 @@ from coin.offers.models import OfferSubscription
 from coin.members.models import Member
 from coin.members.models import Member
 from coin.html2pdf import render_as_pdf
 from coin.html2pdf import render_as_pdf
 from coin.utils import private_files_storage, start_of_month, end_of_month, \
 from coin.utils import private_files_storage, start_of_month, end_of_month, \
-                       disable_for_loaddata, postgresql_regexp
+                       postgresql_regexp
 from coin.isp_database.context_processors import branding
 from coin.isp_database.context_processors import branding
 
 
+accounting_log = logging.getLogger("coin.billing")
+
 
 
 def invoice_pdf_filename(instance, filename):
 def invoice_pdf_filename(instance, filename):
     """Nom et chemin du fichier pdf à stocker pour les factures"""
     """Nom et chemin du fichier pdf à stocker pour les factures"""
@@ -30,6 +29,7 @@ def invoice_pdf_filename(instance, filename):
                                       instance.number,
                                       instance.number,
                                       uuid.uuid4())
                                       uuid.uuid4())
 
 
+
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class InvoiceNumber:
 class InvoiceNumber:
     """ Logic and validation of invoice numbers
     """ Logic and validation of invoice numbers
@@ -139,16 +139,24 @@ class Invoice(models.Model):
                            null=True, blank=True,
                            null=True, blank=True,
                            verbose_name='PDF')
                            verbose_name='PDF')
 
 
-    amount_paid = models.DecimalField(max_digits=5, decimal_places=2, default=0,
+    amount_paid = models.DecimalField(max_digits=5,
+                                      decimal_places=2,
+                                      default=0,
                                       verbose_name='montant payé')
                                       verbose_name='montant payé')
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
+
         # First save to get a PK
         # First save to get a PK
         super(Invoice, self).save(*args, **kwargs)
         super(Invoice, self).save(*args, **kwargs)
-        # Then use that pk to build draft invoice number
-        if not self.validated and self.pk and not self.number:
-            self.number = 'DRAFT-{}'.format(self.pk)
-            self.save()
+
+        if not self.validated:
+            if self.number:
+                accounting_log.info("Creating/updating draft invoice %s."
+                                    % self.number)
+            if not self.number and self.pk:
+                # Use pk to build draft invoice number if needed
+                self.number = 'DRAFT-{}'.format(self.pk)
+                self.save()
 
 
     def amount(self):
     def amount(self):
         """
         """
@@ -161,7 +169,6 @@ class Invoice(models.Model):
         return total.quantize(Decimal('0.01'))
         return total.quantize(Decimal('0.01'))
     amount.short_description = 'Montant'
     amount.short_description = 'Montant'
 
 
-<<<<<<< HEAD
     def amount_before_tax(self):
     def amount_before_tax(self):
         total = Decimal('0.0')
         total = Decimal('0.0')
         for detail in self.details.all():
         for detail in self.details.all():
@@ -180,8 +187,6 @@ class Invoice(models.Model):
         return total.quantize(Decimal('0.01'))
         return total.quantize(Decimal('0.01'))
     amount_paid.short_description = 'Montant payé'
     amount_paid.short_description = 'Montant payé'
 
 
-=======
->>>>>>> Working prototype of automatic payment/invoice reconciliation
     def amount_remaining_to_pay(self):
     def amount_remaining_to_pay(self):
         """
         """
         Calcul le montant restant à payer
         Calcul le montant restant à payer
@@ -213,14 +218,20 @@ class Invoice(models.Model):
         self.date = datetime.date.today()
         self.date = datetime.date.today()
         if not self.date_due:
         if not self.date_due:
             self.date_due = self.date + datetime.timedelta(days=settings.PAYMENT_DELAY)
             self.date_due = self.date + datetime.timedelta(days=settings.PAYMENT_DELAY)
+        old_number = self.number
         self.number = Invoice.objects.get_next_invoice_number(self.date)
         self.number = Invoice.objects.get_next_invoice_number(self.date)
+
         self.validated = True
         self.validated = True
         self.save()
         self.save()
         self.generate_pdf()
         self.generate_pdf()
 
 
+        accounting_log.info("Draft invoice %s validated as invoice %s. "
+                            "(Total amount : %f ; Member : %s)"
+                            % (old_number, self.number, self.amount(), self.member))
         assert self.pdf_exists()
         assert self.pdf_exists()
+        if self.member is not None:
+            update_accounting_for_member(self.member)
 
 
-        update_accounting_for_member(self.member)
 
 
     def pdf_exists(self):
     def pdf_exists(self):
         return (self.validated
         return (self.validated
@@ -302,7 +313,7 @@ class Payment(models.Model):
     amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
     amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
                                  verbose_name='montant')
                                  verbose_name='montant')
     date = models.DateField(default=datetime.date.today)
     date = models.DateField(default=datetime.date.today)
-    invoice = models.ForeignKey(Invoice, verbose_name='facture', null=True,
+    invoice = models.ForeignKey(Invoice, verbose_name='facture associée', null=True,
                                 blank=True, related_name='payments')
                                 blank=True, related_name='payments')
 
 
     amount_already_allocated = models.DecimalField(max_digits=5,
     amount_already_allocated = models.DecimalField(max_digits=5,
@@ -310,43 +321,54 @@ class Payment(models.Model):
                                                    null=True, blank=True, default=0.0,
                                                    null=True, blank=True, default=0.0,
                                                    verbose_name='montant déjà alloué')
                                                    verbose_name='montant déjà alloué')
 
 
-    label_from_bank = models.CharField(max_length=500,
-                                       null=True, blank=True, default="",
-                                       verbose_name='libellé du virement récuppéré via la banque')
+    label = models.CharField(max_length=500,
+                             null=True, blank=True, default="",
+                             verbose_name='libellé')
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         super(Payment, self).save(*args, **kwargs)
         super(Payment, self).save(*args, **kwargs)
-       
+
         # If this payment is related to a member, update the accounting for
         # If this payment is related to a member, update the accounting for
         # this member
         # this member
         if self.member != None:
         if self.member != None:
             update_accounting_for_member(self.member)
             update_accounting_for_member(self.member)
 
 
-
     def amount_not_allocated(self):
     def amount_not_allocated(self):
         return self.amount - self.amount_already_allocated
         return self.amount - self.amount_already_allocated
 
 
-
     @transaction.atomic
     @transaction.atomic
     def allocate_to_invoice(self, invoice):
     def allocate_to_invoice(self, invoice):
         amount_can_pay = self.amount_not_allocated()
         amount_can_pay = self.amount_not_allocated()
         amount_to_pay  = invoice.amount_remaining_to_pay()
         amount_to_pay  = invoice.amount_remaining_to_pay()
         amount_to_allocate = min(amount_can_pay, amount_to_pay)
         amount_to_allocate = min(amount_can_pay, amount_to_pay)
 
 
-        print "Payment "+str(self.date)+" allocating "+str(amount_to_allocate)+" to invoice "+invoice.number
+        accounting_log.info("Allocating %f from payment %s to invoice %s"
+                            % (float(amount_to_allocate), str(self.date),
+                               invoice.number))
 
 
         self.amount_already_allocated += amount_to_allocate
         self.amount_already_allocated += amount_to_allocate
         invoice.amount_paid           += amount_to_allocate
         invoice.amount_paid           += amount_to_allocate
 
 
         # Close invoice if relevant
         # Close invoice if relevant
         if (invoice.amount_remaining_to_pay() <= 0) and (invoice.status == "open"):
         if (invoice.amount_remaining_to_pay() <= 0) and (invoice.status == "open"):
+            accounting_log.info("Invoice %s has been paid and is now closed"
+                                % invoice.number)
             invoice.status = "closed"
             invoice.status = "closed"
 
 
         invoice.save()
         invoice.save()
         self.save()
         self.save()
 
 
+    @property
+    def name(self):
+        return str(self)
+
     def __unicode__(self):
     def __unicode__(self):
-        return 'Paiment de %0.2f€ le %s' % (self.amount, str(self.date))
+        if self.member is not None:
+            return 'Paiment de %0.2f€ le %s par %s' \
+                    % (self.amount, str(self.date), self.member)
+        else:
+            return 'Paiment de %0.2f€ le %s' \
+                    % (self.amount, str(self.date))
 
 
     class Meta:
     class Meta:
         verbose_name = 'paiement'
         verbose_name = 'paiement'
@@ -358,7 +380,10 @@ def update_accounting_for_member(member):
     d'un utilisateur
     d'un utilisateur
     """
     """
 
 
-    print "Member "+str(member)+" currently has a balance of "+str(member.balance)
+    accounting_log.info("Member %s current balance is %f ..."
+                        % (str(member), float(member.balance)))
+    accounting_log.info("Updating accounting for member %s ..."
+                        % str(member))
 
 
     # Fetch relevant and active payments / invoices
     # Fetch relevant and active payments / invoices
     # and sort then by chronological order : olders first, newers last.
     # and sort then by chronological order : olders first, newers last.
@@ -374,19 +399,25 @@ def update_accounting_for_member(member):
     number_of_active_invoices = len([p for p in this_member_invoices if p.amount_remaining_to_pay() > 0])
     number_of_active_invoices = len([p for p in this_member_invoices if p.amount_remaining_to_pay() > 0])
 
 
     if (number_of_active_payments == 0):
     if (number_of_active_payments == 0):
-        print "No active payment for "+str(member)+". Nothing to do."
+        accounting_log.info("(No active payment for %s. No invoice/payment "
+                            "reconciliation needed for now.)."
+                            % str(member))
     elif (number_of_active_invoices == 0):
     elif (number_of_active_invoices == 0):
-        print "No active invoice for "+str(member)+". Nothing to do."
+        accounting_log.info("(No active invoice for %s. No invoice/payment "
+                            "reconciliation needed for now.)."
+                            % str(member))
     else:
     else:
-        print "Initiating reconciliation between invoice and payments for user "+str(member)+"."
-        reconcile_invoices_and_payments(this_member_invoices, this_member_payments)
+        accounting_log.info("Initiating reconciliation between "
+                            "invoice and payments for %s" % str(member))
+        reconcile_invoices_and_payments(this_member_invoices,
+                                        this_member_payments)
 
 
-    member.balance = compute_balance(this_member_invoices, this_member_payments)
+    member.balance = compute_balance(this_member_invoices,
+                                     this_member_payments)
     member.save()
     member.save()
 
 
-    print " "
-    print "Member "+str(member)+" new balance is "+str(member.balance)
-    print " "
+    accounting_log.info("Member %s new balance is %f"
+                        % (str(member),  float(member.balance)))
 
 
 
 
 def reconcile_invoices_and_payments(invoices, payments):
 def reconcile_invoices_and_payments(invoices, payments):
@@ -394,21 +425,19 @@ def reconcile_invoices_and_payments(invoices, payments):
     Rapproche des factures et des paiements qui sont actifs (paiement non alloué
     Rapproche des factures et des paiements qui sont actifs (paiement non alloué
     ou factures non entièrement payées) automatiquement.
     ou factures non entièrement payées) automatiquement.
     Retourne la balance restante (positive si 'trop payé', négative si factures
     Retourne la balance restante (positive si 'trop payé', négative si factures
-    restantes à payer)  
+    restantes à payer)
     """
     """
 
 
     active_payments = [p for p in payments if p.amount_not_allocated()    > 0]
     active_payments = [p for p in payments if p.amount_not_allocated()    > 0]
     active_invoices = [i for i in invoices if i.amount_remaining_to_pay() > 0]
     active_invoices = [i for i in invoices if i.amount_remaining_to_pay() > 0]
 
 
-    #print "Active payment, invoices"
-    #print active_payments
-    #print active_invoices
-
     if (len(active_payments) == 0):
     if (len(active_payments) == 0):
-        print "No more active payment. Nothing to reconcile anymore."
+        accounting_log.info("No more active payment. Nothing to reconcile "
+                            "anymore.")
         return
         return
     if (len(active_invoices) == 0):
     if (len(active_invoices) == 0):
-        print "No more active invoice. Nothing to reconcile anymore."
+        accounting_log.info("No more active invoice. Nothing to reconcile "
+                            "anymore.")
         return
         return
 
 
     # Only consider the oldest active payment and the oldest active invoice
     # Only consider the oldest active payment and the oldest active invoice
@@ -422,7 +451,7 @@ def reconcile_invoices_and_payments(invoices, payments):
     p.allocate_to_invoice(i)
     p.allocate_to_invoice(i)
 
 
     # Reconcicle next payment / invoice
     # Reconcicle next payment / invoice
-    
+
     reconcile_invoices_and_payments(invoices, payments)
     reconcile_invoices_and_payments(invoices, payments)
 
 
 
 
@@ -430,13 +459,14 @@ def compute_balance(invoices, payments):
 
 
     active_payments = [p for p in payments if p.amount_not_allocated()    > 0]
     active_payments = [p for p in payments if p.amount_not_allocated()    > 0]
     active_invoices = [i for i in invoices if i.amount_remaining_to_pay() > 0]
     active_invoices = [i for i in invoices if i.amount_remaining_to_pay() > 0]
-    
+
     s = 0
     s = 0
-    s -= sum([ i.amount_remaining_to_pay() for i in active_invoices ])
-    s += sum([ p.amount_not_allocated()    for p in active_payments ])
+    s -= sum([i.amount_remaining_to_pay() for i in active_invoices])
+    s += sum([p.amount_not_allocated()    for p in active_payments])
 
 
     return s
     return s
 
 
+
 def test_accounting_update():
 def test_accounting_update():
 
 
     Member.objects.all().delete()
     Member.objects.all().delete()
@@ -470,7 +500,6 @@ def test_accounting_update():
     payment = Payment.objects.create(amount=20,
     payment = Payment.objects.create(amount=20,
                                      member=johndoe)
                                      member=johndoe)
 
 
-
     Member.objects.all().delete()
     Member.objects.all().delete()
     Payment.objects.all().delete()
     Payment.objects.all().delete()
     Invoice.objects.all().delete()
     Invoice.objects.all().delete()

+ 1 - 1
coin/members/models.py

@@ -77,7 +77,7 @@ class Member(CoinLdapSyncMixin, AbstractUser):
                         verbose_name="Date du dernier email de relance de cotisation envoyé")
                         verbose_name="Date du dernier email de relance de cotisation envoyé")
 
 
     balance = models.DecimalField(max_digits=5, decimal_places=2, default=0,
     balance = models.DecimalField(max_digits=5, decimal_places=2, default=0,
-                                 verbose_name='account balance')
+                                  verbose_name='account balance')
 
 
 
 
     # Following fields are managed by the parent class AbstractUser :
     # Following fields are managed by the parent class AbstractUser :

+ 1 - 1
coin/members/templates/members/invoices.html

@@ -2,7 +2,7 @@
 
 
 {% block content %}
 {% block content %}
 
 
-<h2>Balance : {{ balance }} €</h2>
+<h2>Balance : {{ balance|floatformat }} €</h2>
 
 
 <h2>Mes factures</h2>
 <h2>Mes factures</h2>
 
 

+ 17 - 0
coin/settings_base.py

@@ -176,6 +176,11 @@ EXTRA_INSTALLED_APPS = tuple()
 LOGGING = {
 LOGGING = {
     'version': 1,
     'version': 1,
     'disable_existing_loggers': False,
     'disable_existing_loggers': False,
+    'formatters': {
+        'verbose': {
+            'format': "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+        },
+    },
     'filters': {
     'filters': {
         'require_debug_false': {
         'require_debug_false': {
             '()': 'django.utils.log.RequireDebugFalse'
             '()': 'django.utils.log.RequireDebugFalse'
@@ -190,6 +195,14 @@ LOGGING = {
         'console': {
         'console': {
             'class': 'logging.StreamHandler',
             'class': 'logging.StreamHandler',
         },
         },
+        'coin_accounting': {
+            'level':'DEBUG',
+            'class':'logging.handlers.RotatingFileHandler',
+            'formatter': 'verbose',
+            'filename': '/opt/coin/accounting.log',
+            'maxBytes': 1024*1024*15, # 15MB
+            'backupCount': 10,
+        },
     },
     },
     'loggers': {
     'loggers': {
         'django.request': {
         'django.request': {
@@ -201,6 +214,10 @@ LOGGING = {
             'handlers': ['console'],
             'handlers': ['console'],
             'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
             'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
         },
         },
+        "coin.billing": {
+            'handlers': ['coin_accounting'],
+            'level': 'DEBUG',
+        }
     }
     }
 }
 }