|
@@ -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()
|