|
@@ -139,6 +139,9 @@ class Invoice(models.Model):
|
|
|
null=True, blank=True,
|
|
|
verbose_name='PDF')
|
|
|
|
|
|
+ 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)
|
|
@@ -158,6 +161,7 @@ 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():
|
|
@@ -176,11 +180,13 @@ 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
|
|
|
"""
|
|
|
- return self.amount() - self.amount_paid()
|
|
|
+ return self.amount() - self.amount_paid
|
|
|
amount_remaining_to_pay.short_description = 'Reste à payer'
|
|
|
|
|
|
def has_owner(self, username):
|
|
@@ -214,6 +220,8 @@ class Invoice(models.Model):
|
|
|
|
|
|
assert self.pdf_exists()
|
|
|
|
|
|
+ update_accounting_for_member(self.member)
|
|
|
+
|
|
|
def pdf_exists(self):
|
|
|
return (self.validated
|
|
|
and bool(self.pdf)
|
|
@@ -231,6 +239,7 @@ class Invoice(models.Model):
|
|
|
|
|
|
objects = InvoiceQuerySet().as_manager()
|
|
|
|
|
|
+
|
|
|
class InvoiceDetail(models.Model):
|
|
|
|
|
|
label = models.CharField(max_length=100)
|
|
@@ -282,9 +291,9 @@ class Payment(models.Model):
|
|
|
)
|
|
|
|
|
|
member = models.ForeignKey(Member, null=True, blank=True, default=None,
|
|
|
- related_name='payment',
|
|
|
- verbose_name='membre',
|
|
|
- on_delete=models.SET_NULL)
|
|
|
+ related_name='payment',
|
|
|
+ verbose_name='membre',
|
|
|
+ on_delete=models.SET_NULL)
|
|
|
|
|
|
payment_mean = models.CharField(max_length=100, null=True,
|
|
|
default='transfer',
|
|
@@ -296,8 +305,33 @@ class Payment(models.Model):
|
|
|
invoice = models.ForeignKey(Invoice, verbose_name='facture', null=True,
|
|
|
blank=True, related_name='payments')
|
|
|
|
|
|
+ amount_already_allocated = models.DecimalField(max_digits=5,
|
|
|
+ decimal_places=2,
|
|
|
+ default=0.0,
|
|
|
+ verbose_name='montant déjà alloué')
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
+ 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"):
|
|
|
+ invoice.status = "closed"
|
|
|
+
|
|
|
+
|
|
|
def __unicode__(self):
|
|
|
- return 'Paiment de %0.2f€' % self.amount
|
|
|
+ return 'Paiment de %0.2f€ le %s' % (self.amount, str(self.date))
|
|
|
|
|
|
class Meta:
|
|
|
verbose_name = 'paiement'
|
|
@@ -305,15 +339,113 @@ class Payment(models.Model):
|
|
|
|
|
|
@receiver(post_save, sender=Payment)
|
|
|
@disable_for_loaddata
|
|
|
-def set_invoice_as_paid_if_needed(sender, instance, **kwargs):
|
|
|
+def update_accounting(sender, instance, **kwargs):
|
|
|
+
|
|
|
+ # Ignore if there's no member set for the invoice/payment
|
|
|
+ member = instance.member
|
|
|
+ if member == None :
|
|
|
+ return
|
|
|
+ else:
|
|
|
+ update_accounting_for_member(member)
|
|
|
+
|
|
|
+
|
|
|
+def update_accounting_for_member(member):
|
|
|
"""
|
|
|
- Lorsqu'un paiement est enregistré, vérifie si la facture est alors
|
|
|
- complétement payée. Dans ce cas elle passe en réglée
|
|
|
+ Met à jour le status des factures, des paiements et le solde du compte
|
|
|
+ d'un utilisateur
|
|
|
"""
|
|
|
- if instance.invoice == None :
|
|
|
- return
|
|
|
|
|
|
- if (instance.invoice.amount_paid() >= instance.invoice.amount() and
|
|
|
- instance.invoice.status == 'open'):
|
|
|
- instance.invoice.status = 'closed'
|
|
|
- instance.invoice.save()
|
|
|
+ # Fetch relevant and active payments / invoices
|
|
|
+ # and sort then by chronological order : olders first, newers last.
|
|
|
+
|
|
|
+ this_member_payments = [p for p in Payment.objects.filter(member=member)
|
|
|
+ .order_by("date")]
|
|
|
+ this_member_invoices = [i for i in Invoice.objects.filter(member=member)
|
|
|
+ .filter(validated=True)
|
|
|
+ .order_by("date")]
|
|
|
+
|
|
|
+ number_of_active_payments = len([p for p in this_member_payments if p.amount_not_allocated() > 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):
|
|
|
+ print "No active payment for "+str(member)+". Nothing to do."
|
|
|
+ elif (number_of_active_invoices == 0):
|
|
|
+ print "No active invoice for "+str(member)+". Nothing to do."
|
|
|
+ else:
|
|
|
+ print "Initiating reconciliation between invoice and payments for user"+str(member)+"."
|
|
|
+ reconcile_invoices_and_payments(this_member_invoices, this_member_payments)
|
|
|
+ print " "
|
|
|
+ print " "
|
|
|
+ print " "
|
|
|
+ print " "
|
|
|
+
|
|
|
+
|
|
|
+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.
|
|
|
+ """
|
|
|
+
|
|
|
+ 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."
|
|
|
+ return
|
|
|
+ if (len(active_invoices) == 0):
|
|
|
+ print "No more active invoice. Nothing to reconcile anymore."
|
|
|
+ return
|
|
|
+
|
|
|
+ # Only consider the oldest active payment and the oldest active invoice
|
|
|
+
|
|
|
+ p = active_payments[0]
|
|
|
+ i = active_invoices[0]
|
|
|
+
|
|
|
+ # TODO : should add an assert that the ammount not allocated / remaining to
|
|
|
+ # pay is lower before and after calling the allocate_to_invoice
|
|
|
+
|
|
|
+ p.allocate_to_invoice(i)
|
|
|
+
|
|
|
+ # Reconcicle next payment / invoice
|
|
|
+
|
|
|
+ reconcile_invoices_and_payments(invoices, payments)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+def test_accounting_update():
|
|
|
+
|
|
|
+ Member.objects.all().delete()
|
|
|
+ Payment.objects.all().delete()
|
|
|
+ Invoice.objects.all().delete()
|
|
|
+
|
|
|
+ johndoe = Member.objects.create(username="johndoe",
|
|
|
+ first_name="John",
|
|
|
+ last_name="Doe",
|
|
|
+ email="johndoe@yolo.test")
|
|
|
+ johndoe.set_password("trololo")
|
|
|
+
|
|
|
+
|
|
|
+ # First facture
|
|
|
+ invoice = Invoice.objects.create(number="1337",
|
|
|
+ member=johndoe)
|
|
|
+ InvoiceDetail.objects.create(label="superservice",
|
|
|
+ amount="15.0",
|
|
|
+ invoice=invoice)
|
|
|
+ invoice.validate()
|
|
|
+
|
|
|
+ # Second facture
|
|
|
+ invoice2 = Invoice.objects.create(number="42",
|
|
|
+ member=johndoe)
|
|
|
+ InvoiceDetail.objects.create(label="superservice",
|
|
|
+ amount="42",
|
|
|
+ invoice=invoice2)
|
|
|
+ invoice2.validate()
|
|
|
+
|
|
|
+ # Payment
|
|
|
+ payment = Payment.objects.create(amount=20,
|
|
|
+ member=johndoe)
|
|
|
+
|