Browse Source

Adding logging for billing

Alexandre Aubin 7 years ago
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:
 
                 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"])):
                     foundMatch = True
                     break
@@ -301,7 +301,7 @@ should run this command with --commit if you agree with the dry-run."""
             print newPayment
             # Create the payment
             payment = Payment.objects.create(amount=float(newPayment["amount"]),
-                                             label_from_bank=str(newPayment["label"]),
+                                             label=str(newPayment["label"]),
                                              date=str(newPayment["date"]),
                                              member=member)
 

+ 73 - 44
coin/billing/models.py

@@ -2,16 +2,13 @@
 from __future__ import unicode_literals
 
 import datetime
-import random
 import uuid
-import os
 import re
+import logging
 from decimal import Decimal
 
 from django.conf import settings
 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
 
 
@@ -19,9 +16,11 @@ 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, \
-                       disable_for_loaddata, postgresql_regexp
+                       postgresql_regexp
 from coin.isp_database.context_processors import branding
 
+accounting_log = logging.getLogger("coin.billing")
+
 
 def invoice_pdf_filename(instance, filename):
     """Nom et chemin du fichier pdf à stocker pour les factures"""
@@ -30,6 +29,7 @@ def invoice_pdf_filename(instance, filename):
                                       instance.number,
                                       uuid.uuid4())
 
+
 @python_2_unicode_compatible
 class InvoiceNumber:
     """ Logic and validation of invoice numbers
@@ -139,16 +139,24 @@ class Invoice(models.Model):
                            null=True, blank=True,
                            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é')
 
     def save(self, *args, **kwargs):
+
         # First save to get a PK
         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):
         """
@@ -161,7 +169,6 @@ class Invoice(models.Model):
         return total.quantize(Decimal('0.01'))
     amount.short_description = 'Montant'
 
-<<<<<<< HEAD
     def amount_before_tax(self):
         total = Decimal('0.0')
         for detail in self.details.all():
@@ -180,8 +187,6 @@ class Invoice(models.Model):
         return total.quantize(Decimal('0.01'))
     amount_paid.short_description = 'Montant payé'
 
-=======
->>>>>>> Working prototype of automatic payment/invoice reconciliation
     def amount_remaining_to_pay(self):
         """
         Calcul le montant restant à payer
@@ -213,14 +218,20 @@ class Invoice(models.Model):
         self.date = datetime.date.today()
         if not self.date_due:
             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.validated = True
         self.save()
         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()
+        if self.member is not None:
+            update_accounting_for_member(self.member)
 
-        update_accounting_for_member(self.member)
 
     def pdf_exists(self):
         return (self.validated
@@ -302,7 +313,7 @@ class Payment(models.Model):
     amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
                                  verbose_name='montant')
     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')
 
     amount_already_allocated = models.DecimalField(max_digits=5,
@@ -310,43 +321,54 @@ class Payment(models.Model):
                                                    null=True, blank=True, default=0.0,
                                                    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):
         super(Payment, self).save(*args, **kwargs)
-       
+
         # If this payment is related to a member, update the accounting for
         # this member
         if self.member != None:
             update_accounting_for_member(self.member)
 
-
     def amount_not_allocated(self):
         return self.amount - self.amount_already_allocated
 
-
     @transaction.atomic
     def allocate_to_invoice(self, invoice):
         amount_can_pay = self.amount_not_allocated()
         amount_to_pay  = invoice.amount_remaining_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
         invoice.amount_paid           += amount_to_allocate
 
         # Close invoice if relevant
         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.save()
         self.save()
 
+    @property
+    def name(self):
+        return str(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:
         verbose_name = 'paiement'
@@ -358,7 +380,10 @@ def update_accounting_for_member(member):
     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
     # 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])
 
     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):
-        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:
-        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()
 
-    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):
@@ -394,21 +425,19 @@ def reconcile_invoices_and_payments(invoices, payments):
     Rapproche des factures et des paiements qui sont actifs (paiement non alloué
     ou factures non entièrement payées) automatiquement.
     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_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):
-        print "No more active payment. Nothing to reconcile anymore."
+        accounting_log.info("No more active payment. Nothing to reconcile "
+                            "anymore.")
         return
     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
 
     # 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)
 
     # Reconcicle next payment / invoice
-    
+
     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_invoices = [i for i in invoices if i.amount_remaining_to_pay() > 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
 
+
 def test_accounting_update():
 
     Member.objects.all().delete()
@@ -470,7 +500,6 @@ def test_accounting_update():
     payment = Payment.objects.create(amount=20,
                                      member=johndoe)
 
-
     Member.objects.all().delete()
     Payment.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é")
 
     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 :

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

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

+ 17 - 0
coin/settings_base.py

@@ -176,6 +176,11 @@ EXTRA_INSTALLED_APPS = tuple()
 LOGGING = {
     'version': 1,
     'disable_existing_loggers': False,
+    'formatters': {
+        'verbose': {
+            'format': "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+        },
+    },
     'filters': {
         'require_debug_false': {
             '()': 'django.utils.log.RequireDebugFalse'
@@ -190,6 +195,14 @@ LOGGING = {
         'console': {
             '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': {
         'django.request': {
@@ -201,6 +214,10 @@ LOGGING = {
             'handlers': ['console'],
             'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
         },
+        "coin.billing": {
+            'handlers': ['coin_accounting'],
+            'level': 'DEBUG',
+        }
     }
 }