Browse Source

Adding a PaymentAllocation class to keep track of payment allocations..

Alexandre Aubin 7 years ago
parent
commit
8f09e71253
2 changed files with 73 additions and 45 deletions
  1. 39 20
      coin/billing/admin.py
  2. 34 25
      coin/billing/models.py

+ 39 - 20
coin/billing/admin.py

@@ -8,7 +8,7 @@ from django.conf.urls import url
 from django.contrib.admin.util import flatten_fieldsets
 
 from coin.filtering_queryset import LimitedAdminInlineMixin
-from coin.billing.models import Invoice, InvoiceDetail, Payment
+from coin.billing.models import Invoice, InvoiceDetail, Payment, PaymentAllocation
 from coin.billing.utils import get_invoice_from_id_or_number
 from django.core.urlresolvers import reverse
 import autocomplete_light
@@ -64,31 +64,31 @@ class InvoiceDetailInlineReadOnly(admin.StackedInline):
         return result
 
 
-class PaymentInlineAdd(admin.StackedInline):
-    model = Payment
+class PaymentAllocatedReadOnly(admin.TabularInline):
+    model = PaymentAllocation
     extra = 0
-    fields = (('date', 'payment_mean', 'amount'),)
-    can_delete = False
+    fields = ("payment", "amount")
+    readonly_fields = ("payment", "amount")
+    verbose_name = None
+    verbose_name_plural = "Paiement alloués"
 
-    verbose_name_plural = "Ajouter des paiements"
+    def has_add_permission(self, request, obj=None):
+        return False
 
-    def has_change_permission(self, request):
+    def has_delete_permission(self, request, obj=None):
         return False
 
 
-class PaymentInlineReadOnly(admin.StackedInline):
+class PaymentInlineAdd(admin.StackedInline):
     model = Payment
     extra = 0
-    fields = PaymentInlineAdd.fields
+    fields = (('date', 'payment_mean', 'amount'),)
     can_delete = False
-    verbose_name = None
-    verbose_name_plural = "Paiements"
 
-    def has_add_permission(self, request):
-        return False
+    verbose_name_plural = "Ajouter des paiements"
 
-    def get_readonly_fields(self, request, obj=None):
-        return flatten_fieldsets(self.declared_fieldsets)
+    def has_change_permission(self, request):
+        return False
 
 
 class InvoiceAdmin(admin.ModelAdmin):
@@ -133,9 +133,10 @@ class InvoiceAdmin(admin.ModelAdmin):
             else:
                 inlines = [InvoiceDetailInline]
 
-            if obj.status == 'open' and obj.validated:
-                inlines += [PaymentInlineReadOnly]
-                inlines += [PaymentInlineAdd]
+            if obj.validated:
+                inlines += [PaymentAllocatedReadOnly]
+                if obj.status == "open":
+                    inlines += [PaymentInlineAdd]
 
         for inline_class in inlines:
             inline = inline_class(self.model, self.admin_site)
@@ -187,13 +188,29 @@ class InvoiceAdmin(admin.ModelAdmin):
                                             args=(id,)))
 
 
+class PaymentAllocationInlineReadOnly(admin.TabularInline):
+    model = PaymentAllocation
+    extra = 0
+    fields = ("invoice", "amount")
+    readonly_fields = ("invoice", "amount")
+    verbose_name = None
+    verbose_name_plural = "Alloué à"
+
+    def has_add_permission(self, request, obj=None):
+        return False
+
+    def has_delete_permission(self, request, obj=None):
+        return False
+
+
 class PaymentAdmin(admin.ModelAdmin):
 
     list_display = ('__unicode__', 'member', 'payment_mean', 'amount', 'date',
-                    'invoice', 'amount_already_allocated', 'label')
+                    'amount_already_allocated', 'label')
     list_display_links = ()
     fields = (('member'),
-              ('amount', 'payment_mean', 'date', 'label'))
+              ('amount', 'payment_mean', 'date', 'label'),
+              ('amount_already_allocated'))
     readonly_fields = ('amount_already_allocated', 'label')
     form = autocomplete_light.modelform_factory(Payment, fields='__all__')
 
@@ -206,6 +223,8 @@ class PaymentAdmin(admin.ModelAdmin):
         else:
             return self.readonly_fields
 
+    def get_inline_instances(self, request, obj=None):
+        return [PaymentAllocationInlineReadOnly(self.model, self.admin_site)]
 
 admin.site.register(Invoice, InvoiceAdmin)
 admin.site.register(Payment, PaymentAdmin)

+ 34 - 25
coin/billing/models.py

@@ -148,11 +148,6 @@ 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é')
-
     date_last_reminder_email = models.DateTimeField(null=True, blank=True,
                         verbose_name="Date du dernier email de relance envoyé")
 
@@ -184,20 +179,16 @@ class Invoice(models.Model):
 
     def amount_paid(self):
         """
-        Calcul le montant payé de la facture en fonction des éléments
-        de paiements
+        Calcul le montant déjà payé à partir des allocations de paiements
         """
-        total = Decimal('0.0')
-        for payment in self.payments.all():
-            total += payment.amount
-        return total.quantize(Decimal('0.01'))
+        return sum([a.amount for a in self.allocations.all()])
     amount_paid.short_description = 'Montant payé'
 
     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):
@@ -380,11 +371,6 @@ class Payment(models.Model):
     invoice = models.ForeignKey(Invoice, verbose_name='facture associée', null=True,
                                 blank=True, related_name='payments')
 
-    amount_already_allocated = models.DecimalField(max_digits=5,
-                                                   decimal_places=2,
-                                                   null=True, blank=True, default=0.0,
-                                                   verbose_name='montant déjà alloué')
-
     label = models.CharField(max_length=500,
                              null=True, blank=True, default="",
                              verbose_name='libellé')
@@ -392,7 +378,7 @@ class Payment(models.Model):
     def save(self, *args, **kwargs):
 
         # Only if no amount already allocated...
-        if self.amount_already_allocated == 0:
+        if self.amount_already_allocated() == 0:
 
             # If there's a linked invoice and no member defined
             if self.invoice and not self.member:
@@ -404,20 +390,26 @@ class Payment(models.Model):
 
     def clean(self):
 
-        # Only if no amount already allocated...
-        if self.amount_already_allocated == 0:
+        # Only if no amount already alloca ted...
+        if self.amount_already_allocated() == 0:
 
             # If there's a linked invoice and this payment would pay more than
             # the remaining amount needed to pay the invoice...
             if self.invoice and self.amount > self.invoice.amount_remaining_to_pay():
                 raise ValidationError("This payment would pay more than the invoice's remaining to pay")
 
+    def amount_already_allocated(self):
+        return sum([ a.amount for a in self.allocations.all() ])
 
     def amount_not_allocated(self):
-        return self.amount - self.amount_already_allocated
+        return self.amount - self.amount_already_allocated()
 
     @transaction.atomic
     def allocate_to_invoice(self, invoice):
+
+        # FIXME - Add asserts about remaining amount > 0, unpaid amount > 0,
+        # ...
+
         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)
@@ -426,8 +418,9 @@ class Payment(models.Model):
                             % (float(amount_to_allocate), str(self.date),
                                invoice.number))
 
-        self.amount_already_allocated += amount_to_allocate
-        invoice.amount_paid           += amount_to_allocate
+        PaymentAllocation.objects.create(invoice=invoice,
+                                         payment=self,
+                                         amount=amount_to_allocate)
 
         # Close invoice if relevant
         if (invoice.amount_remaining_to_pay() <= 0) and (invoice.status == "open"):
@@ -450,6 +443,22 @@ class Payment(models.Model):
         verbose_name = 'paiement'
 
 
+# This corresponds to a (possibly partial) allocation of a given payment to
+# a given invoice.
+# E.g. consider an invoice I with total 15€ and a payment P with 10€.
+# There can be for example an allocation of 3.14€ from P to I.
+class PaymentAllocation(models.Model):
+
+    invoice = models.ForeignKey(Invoice, verbose_name='facture associée',
+                                null=False, blank=False,
+                                related_name='allocations')
+    payment = models.ForeignKey(Payment, verbose_name='facture associée',
+                                null=False, blank=False,
+                                related_name='allocations')
+    amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
+                                 verbose_name='montant')
+
+
 def get_active_payment_and_invoices(member):
 
     # Fetch relevant and active payments / invoices
@@ -566,7 +575,7 @@ def payment_changed(sender, instance, created, **kwargs):
         accounting_log.info("Updating payment %s (Date: %s, Member: %s, Amount: %s, Label: %s, Allocated: %s)."
                             % (instance.pk, instance.date, instance.member,
                                 instance.amount, instance.label.encode('utf-8'),
-                                instance.amount_already_allocated))
+                                instance.amount_already_allocated()))
 
     # If this payment is related to a member, update the accounting for
     # this member
@@ -588,7 +597,7 @@ def invoice_changed(sender, instance, created, **kwargs):
                     % (instance.number, instance.member))
         else:
             accounting_log.info("Updating invoice %s (Member: %s, Total amount: %s, Amount paid: %s)."
-                    % (instance.number, instance.member, instance.amount(), instance.amount_paid ))
+                    % (instance.number, instance.member, instance.amount(), instance.amount_paid() ))
 
 
 def test_accounting_update():