|
@@ -5,6 +5,7 @@ import datetime
|
|
|
import logging
|
|
|
import uuid
|
|
|
import re
|
|
|
+import abc
|
|
|
from decimal import Decimal
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
|
@@ -29,13 +30,116 @@ from coin.isp_database.models import ISPInfo
|
|
|
accounting_log = logging.getLogger("coin.billing")
|
|
|
|
|
|
|
|
|
-def invoice_pdf_filename(instance, filename):
|
|
|
+def bill_pdf_filename(instance, filename):
|
|
|
"""Nom et chemin du fichier pdf à stocker pour les factures"""
|
|
|
member_id = instance.member.id if instance.member else 0
|
|
|
return 'invoices/%d_%s_%s.pdf' % (member_id,
|
|
|
instance.number,
|
|
|
uuid.uuid4())
|
|
|
|
|
|
+def invoice_pdf_filename(instance, filename):
|
|
|
+ return bill_pdf_filename(instance, filename)
|
|
|
+
|
|
|
+class Bill(models.Model):
|
|
|
+
|
|
|
+ BILL_STATUS_CHOICES = (
|
|
|
+ ('open', 'À payer'),
|
|
|
+ ('closed', 'Réglée'),
|
|
|
+ ('trouble', 'Litige')
|
|
|
+ )
|
|
|
+
|
|
|
+ status = models.CharField(max_length=50, choices=BILL_STATUS_CHOICES,
|
|
|
+ default='open',
|
|
|
+ verbose_name='statut')
|
|
|
+ date = models.DateField(
|
|
|
+ default=datetime.date.today, null=True, verbose_name='date',
|
|
|
+ help_text='Cette date sera définie à la date de validation dans le document final')
|
|
|
+ member = models.ForeignKey(Member, null=True, blank=True, default=None,
|
|
|
+ related_name='bills',
|
|
|
+ verbose_name='membre',
|
|
|
+ on_delete=models.SET_NULL)
|
|
|
+ pdf = models.FileField(storage=private_files_storage,
|
|
|
+ upload_to=bill_pdf_filename,
|
|
|
+ null=True, blank=True,
|
|
|
+ verbose_name='PDF')
|
|
|
+ @property
|
|
|
+ def amount(self):
|
|
|
+ """ Return bill amount """
|
|
|
+ return self.cast.amount
|
|
|
+ amount.fget.short_description = 'Montant'
|
|
|
+
|
|
|
+ def amount_paid(self):
|
|
|
+ """
|
|
|
+ Calcul le montant déjà payé à partir des allocations de paiements
|
|
|
+ """
|
|
|
+ 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()
|
|
|
+ amount_remaining_to_pay.short_description = 'Reste à payer'
|
|
|
+
|
|
|
+ def has_owner(self, username):
|
|
|
+ """
|
|
|
+ Check if passed username (ex gmajax) is owner of the invoice
|
|
|
+ """
|
|
|
+ return (self.member and self.member.username == username)
|
|
|
+
|
|
|
+ def generate_pdf(self):
|
|
|
+ """
|
|
|
+ Make and store a pdf file for the invoice
|
|
|
+ """
|
|
|
+ context = {"invoice": self}
|
|
|
+ context.update(branding(None))
|
|
|
+ pdf_file = render_as_pdf('billing/invoice_pdf.html', context)
|
|
|
+ self.pdf.save('%s.pdf' % self.number, pdf_file)
|
|
|
+
|
|
|
+ def pdf_exists(self):
|
|
|
+ return (bool(self.pdf)
|
|
|
+ and private_files_storage.exists(self.pdf.name))
|
|
|
+
|
|
|
+# def get_absolute_url(self):
|
|
|
+# return reverse('billing:invoice', args=[self.number])
|
|
|
+
|
|
|
+ def __unicode__(self):
|
|
|
+ return '%s - %s - %i€' % (self.member, self.date, self.amount)
|
|
|
+
|
|
|
+ @property
|
|
|
+ def reference(self):
|
|
|
+ if hasattr(self, 'membershipfee'):
|
|
|
+ return 'Cotisation'
|
|
|
+ elif hasattr(self, 'donation'):
|
|
|
+ return 'Don'
|
|
|
+ elif hasattr(self, 'invoice'):
|
|
|
+ return self.invoice.number
|
|
|
+
|
|
|
+ def log_change(self, created):
|
|
|
+
|
|
|
+ if created:
|
|
|
+ accounting_log.info("Creating %s %s (Member: %s)."
|
|
|
+ % (self.pk, self.member))
|
|
|
+ else:
|
|
|
+ accounting_log.info("Updating %s %s (Member: %s, Total amount: %s, Amount paid: %s)."
|
|
|
+ % (self.pk, self.member, self.amount, self.amount_paid() ))
|
|
|
+
|
|
|
+ @property
|
|
|
+ def cast(bill):
|
|
|
+ if hasattr(bill, 'membershipfee'):
|
|
|
+ return bill.membershipfee
|
|
|
+ elif hasattr(bill, 'donation'):
|
|
|
+ return bill.donation
|
|
|
+ elif hasattr(bill, 'invoice'):
|
|
|
+ return bill.invoice
|
|
|
+ @staticmethod
|
|
|
+ def get_member_validated_bills(member):
|
|
|
+ related_fields = ['membershipfee', 'donation', 'invoice']
|
|
|
+ return [i.cast for i in member.bills.order_by("date") if i.cast.validated]
|
|
|
+
|
|
|
+ class Meta:
|
|
|
+ verbose_name = 'note'
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
|
class InvoiceNumber:
|
|
@@ -117,13 +221,8 @@ class InvoiceQuerySet(models.QuerySet):
|
|
|
InvoiceNumber.RE_INVOICE_NUMBER))
|
|
|
|
|
|
|
|
|
-class Invoice(models.Model):
|
|
|
+class Invoice(Bill):
|
|
|
|
|
|
- INVOICES_STATUS_CHOICES = (
|
|
|
- ('open', 'À payer'),
|
|
|
- ('closed', 'Réglée'),
|
|
|
- ('trouble', 'Litige')
|
|
|
- )
|
|
|
|
|
|
validated = models.BooleanField(default=False, verbose_name='validée',
|
|
|
help_text='Once validated, a PDF is generated'
|
|
@@ -131,24 +230,10 @@ class Invoice(models.Model):
|
|
|
number = models.CharField(max_length=25,
|
|
|
unique=True,
|
|
|
verbose_name='numéro')
|
|
|
- status = models.CharField(max_length=50, choices=INVOICES_STATUS_CHOICES,
|
|
|
- default='open',
|
|
|
- verbose_name='statut')
|
|
|
- date = models.DateField(
|
|
|
- default=datetime.date.today, null=True, verbose_name='date',
|
|
|
- help_text='Cette date sera définie à la date de validation dans la facture finale')
|
|
|
date_due = models.DateField(
|
|
|
null=True, blank=True,
|
|
|
verbose_name="date d'échéance de paiement",
|
|
|
help_text='Le délai de paiement sera fixé à {} jours à la validation si laissé vide'.format(settings.PAYMENT_DELAY))
|
|
|
- member = models.ForeignKey(Member, null=True, blank=True, default=None,
|
|
|
- related_name='invoices',
|
|
|
- verbose_name='membre',
|
|
|
- on_delete=models.SET_NULL)
|
|
|
- pdf = models.FileField(storage=private_files_storage,
|
|
|
- upload_to=invoice_pdf_filename,
|
|
|
- null=True, blank=True,
|
|
|
- verbose_name='PDF')
|
|
|
|
|
|
date_last_reminder_email = models.DateTimeField(null=True, blank=True,
|
|
|
verbose_name="Date du dernier email de relance envoyé")
|
|
@@ -161,6 +246,7 @@ class Invoice(models.Model):
|
|
|
self.number = 'DRAFT-{}'.format(self.pk)
|
|
|
self.save()
|
|
|
|
|
|
+ @property
|
|
|
def amount(self):
|
|
|
"""
|
|
|
Calcul le montant de la facture
|
|
@@ -170,7 +256,7 @@ class Invoice(models.Model):
|
|
|
for detail in self.details.all():
|
|
|
total += detail.total()
|
|
|
return total.quantize(Decimal('0.01'))
|
|
|
- amount.short_description = 'Montant'
|
|
|
+ amount.fget.short_description = 'Montant'
|
|
|
|
|
|
def amount_before_tax(self):
|
|
|
total = Decimal('0.0')
|
|
@@ -179,34 +265,6 @@ class Invoice(models.Model):
|
|
|
return total.quantize(Decimal('0.01'))
|
|
|
amount_before_tax.short_description = 'Montant HT'
|
|
|
|
|
|
- def amount_paid(self):
|
|
|
- """
|
|
|
- Calcul le montant déjà payé à partir des allocations de paiements
|
|
|
- """
|
|
|
- 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()
|
|
|
- amount_remaining_to_pay.short_description = 'Reste à payer'
|
|
|
-
|
|
|
- def has_owner(self, username):
|
|
|
- """
|
|
|
- Check if passed username (ex gmajax) is owner of the invoice
|
|
|
- """
|
|
|
- return (self.member and self.member.username == username)
|
|
|
-
|
|
|
- def generate_pdf(self):
|
|
|
- """
|
|
|
- Make and store a pdf file for the invoice
|
|
|
- """
|
|
|
- context = {"invoice": self}
|
|
|
- context.update(branding(None))
|
|
|
- pdf_file = render_as_pdf('billing/invoice_pdf.html', context)
|
|
|
- self.pdf.save('%s.pdf' % self.number, pdf_file)
|
|
|
|
|
|
@transaction.atomic
|
|
|
def validate(self):
|
|
@@ -226,7 +284,7 @@ class Invoice(models.Model):
|
|
|
|
|
|
accounting_log.info("Draft invoice %s validated as invoice %s. "
|
|
|
"(Total amount : %f ; Member : %s)"
|
|
|
- % (old_number, self.number, self.amount(), self.member))
|
|
|
+ % (old_number, self.number, self.amount, self.member))
|
|
|
assert self.pdf_exists()
|
|
|
if self.member is not None:
|
|
|
update_accounting_for_member(self.member)
|
|
@@ -237,11 +295,8 @@ class Invoice(models.Model):
|
|
|
and bool(self.pdf)
|
|
|
and private_files_storage.exists(self.pdf.name))
|
|
|
|
|
|
- def get_absolute_url(self):
|
|
|
- return reverse('billing:invoice', args=[self.number])
|
|
|
-
|
|
|
def __unicode__(self):
|
|
|
- return '#%s %0.2f€ %s' % (self.number, self.amount(), self.date_due)
|
|
|
+ return '#%s %0.2f€ %s' % (self.number, self.amount, self.date_due)
|
|
|
|
|
|
def reminder_needed(self):
|
|
|
|
|
@@ -302,6 +357,18 @@ class Invoice(models.Model):
|
|
|
self.save()
|
|
|
return True
|
|
|
|
|
|
+ def log_change(self, created):
|
|
|
+
|
|
|
+ if created:
|
|
|
+ accounting_log.info("Creating draft invoice %s (Member: %s)."
|
|
|
+ % ('DRAFT-{}'.format(self.pk), self.member))
|
|
|
+ else:
|
|
|
+ if not self.validated:
|
|
|
+ accounting_log.info("Updating draft invoice %s (Member: %s)."
|
|
|
+ % (self.number, self.member))
|
|
|
+ else:
|
|
|
+ accounting_log.info("Updating invoice %s (Member: %s, Total amount: %s, Amount paid: %s)."
|
|
|
+ % (self.number, self.member, self.amount, self.amount_paid() ))
|
|
|
class Meta:
|
|
|
verbose_name = 'facture'
|
|
|
|
|
@@ -349,6 +416,69 @@ class InvoiceDetail(models.Model):
|
|
|
verbose_name = 'détail de facture'
|
|
|
|
|
|
|
|
|
+class Donation(Bill):
|
|
|
+ _amount = models.DecimalField(max_digits=8, decimal_places=2,
|
|
|
+ verbose_name='Montant')
|
|
|
+
|
|
|
+ @property
|
|
|
+ def amount(self):
|
|
|
+ return self._amount
|
|
|
+ amount.fget.short_description = 'Montant'
|
|
|
+
|
|
|
+ @property
|
|
|
+ def validated(self):
|
|
|
+ return True
|
|
|
+
|
|
|
+ def save(self, *args, **kwargs):
|
|
|
+
|
|
|
+ super(Donation, self).save(*args, **kwargs)
|
|
|
+
|
|
|
+ def clean(self):
|
|
|
+ # Only if no amount already allocated...
|
|
|
+ if not self.member or self.member.balance < self.amount:
|
|
|
+ raise ValidationError("Le solde n'est pas suffisant pour payer ce don. \
|
|
|
+ Merci de commencer par enregistrer un paiement pour ce membre.")
|
|
|
+ class Meta:
|
|
|
+ verbose_name = 'don'
|
|
|
+
|
|
|
+class MembershipFee(Bill):
|
|
|
+ _amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
|
|
|
+ default=settings.MEMBER_DEFAULT_COTISATION,
|
|
|
+ verbose_name='montant', help_text='en €')
|
|
|
+ start_date = models.DateField(
|
|
|
+ null=False,
|
|
|
+ blank=False,
|
|
|
+ verbose_name='date de début de cotisation')
|
|
|
+ end_date = models.DateField(
|
|
|
+ null=False,
|
|
|
+ blank=True,
|
|
|
+ verbose_name='date de fin de cotisation',
|
|
|
+ help_text='par défaut, la cotisation dure un an')
|
|
|
+
|
|
|
+ @property
|
|
|
+ def amount(self):
|
|
|
+ return self._amount
|
|
|
+ amount.fget.short_description = 'Montant'
|
|
|
+ @property
|
|
|
+ def validated(self):
|
|
|
+ return True
|
|
|
+
|
|
|
+ def save(self, *args, **kwargs):
|
|
|
+
|
|
|
+ super(MembershipFee, self).save(*args, **kwargs)
|
|
|
+
|
|
|
+
|
|
|
+ def clean(self):
|
|
|
+ if self.start_date is not None and self.end_date is None:
|
|
|
+ self.end_date = self.start_date + datetime.timedelta(364)
|
|
|
+ # Only if no amount already allocated...
|
|
|
+ if not self.member or self.member.balance < self.amount:
|
|
|
+ raise ValidationError("Le solde n'est pas suffisant pour payer cette cotisation. \
|
|
|
+ Merci de commencer par enregistrer un paiement pour ce membre.")
|
|
|
+
|
|
|
+ class Meta:
|
|
|
+ verbose_name = 'cotisation'
|
|
|
+
|
|
|
class Payment(models.Model):
|
|
|
|
|
|
PAYMENT_MEAN_CHOICES = (
|
|
@@ -370,7 +500,7 @@ class Payment(models.Model):
|
|
|
amount = models.DecimalField(max_digits=6, decimal_places=2, null=True,
|
|
|
verbose_name='montant')
|
|
|
date = models.DateField(default=datetime.date.today)
|
|
|
- invoice = models.ForeignKey(Invoice, verbose_name='facture associée', null=True,
|
|
|
+ bill = models.ForeignKey(Bill, verbose_name='facture associée', null=True,
|
|
|
blank=True, related_name='payments')
|
|
|
|
|
|
label = models.CharField(max_length=500,
|
|
@@ -383,9 +513,9 @@ class Payment(models.Model):
|
|
|
if self.amount_already_allocated() == 0:
|
|
|
|
|
|
# If there's a linked invoice and no member defined
|
|
|
- if self.invoice and not self.member:
|
|
|
+ if self.bill and not self.member:
|
|
|
# Automatically set member to invoice's member
|
|
|
- self.member = self.invoice.member
|
|
|
+ self.member = self.bill.member
|
|
|
|
|
|
super(Payment, self).save(*args, **kwargs)
|
|
|
|
|
@@ -397,7 +527,7 @@ class Payment(models.Model):
|
|
|
|
|
|
# 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():
|
|
|
+ if self.bill and self.amount > self.bill.amount_remaining_to_pay():
|
|
|
raise ValidationError("This payment would pay more than the invoice's remaining to pay")
|
|
|
|
|
|
def amount_already_allocated(self):
|
|
@@ -407,30 +537,30 @@ class Payment(models.Model):
|
|
|
return self.amount - self.amount_already_allocated()
|
|
|
|
|
|
@transaction.atomic
|
|
|
- def allocate_to_invoice(self, invoice):
|
|
|
+ def allocate_to_bill(self, bill):
|
|
|
|
|
|
# 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_pay = bill.amount_remaining_to_pay()
|
|
|
amount_to_allocate = min(amount_can_pay, amount_to_pay)
|
|
|
|
|
|
- accounting_log.info("Allocating %f from payment %s to invoice %s"
|
|
|
+ accounting_log.info("Allocating %f from payment %s to bill %s %s"
|
|
|
% (float(amount_to_allocate), str(self.date),
|
|
|
- invoice.number))
|
|
|
+ bill.reference, bill.pk))
|
|
|
|
|
|
- PaymentAllocation.objects.create(invoice=invoice,
|
|
|
+ PaymentAllocation.objects.create(bill=bill,
|
|
|
payment=self,
|
|
|
amount=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"
|
|
|
+ if (bill.amount_remaining_to_pay() <= 0) and (bill.status == "open"):
|
|
|
+ accounting_log.info("Bill %s %s has been paid and is now closed"
|
|
|
+ % (bill.reference, bill.pk))
|
|
|
+ bill.status = "closed"
|
|
|
|
|
|
- invoice.save()
|
|
|
+ bill.save()
|
|
|
self.save()
|
|
|
|
|
|
def __unicode__(self):
|
|
@@ -451,7 +581,7 @@ class Payment(models.Model):
|
|
|
# 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',
|
|
|
+ bill = models.ForeignKey(Bill, verbose_name='facture associée',
|
|
|
null=False, blank=False,
|
|
|
related_name='allocations')
|
|
|
payment = models.ForeignKey(Payment, verbose_name='facture associée',
|
|
@@ -461,21 +591,21 @@ class PaymentAllocation(models.Model):
|
|
|
verbose_name='montant')
|
|
|
|
|
|
|
|
|
-def get_active_payment_and_invoices(member):
|
|
|
+def get_active_payment_and_bills(member):
|
|
|
|
|
|
# Fetch relevant and active payments / invoices
|
|
|
# and sort then by chronological order : olders first, newers last.
|
|
|
|
|
|
- this_member_invoices = [i for i in member.invoices.filter(validated=True).order_by("date")]
|
|
|
+ this_member_bills = Bill.get_member_validated_bills(member)
|
|
|
this_member_payments = [p for p in member.payments.order_by("date")]
|
|
|
|
|
|
# TODO / FIXME ^^^ maybe also consider only 'opened' invoices (i.e. not
|
|
|
# conflict / trouble invoices)
|
|
|
|
|
|
active_payments = [p for p in this_member_payments if p.amount_not_allocated() > 0]
|
|
|
- active_invoices = [p for p in this_member_invoices if p.amount_remaining_to_pay() > 0]
|
|
|
+ active_bills = [p for p in this_member_bills if p.amount_remaining_to_pay() > 0]
|
|
|
|
|
|
- return active_payments, active_invoices
|
|
|
+ return active_payments, active_bills
|
|
|
|
|
|
|
|
|
def update_accounting_for_member(member):
|
|
@@ -489,12 +619,12 @@ def update_accounting_for_member(member):
|
|
|
accounting_log.info("Member %s current balance is %f ..."
|
|
|
% (member, float(member.balance)))
|
|
|
|
|
|
- reconcile_invoices_and_payments(member)
|
|
|
+ reconcile_bills_and_payments(member)
|
|
|
|
|
|
- this_member_invoices = [i for i in member.invoices.filter(validated=True).order_by("date")]
|
|
|
+ this_member_bills = Bill.get_member_validated_bills(member)
|
|
|
this_member_payments = [p for p in member.payments.order_by("date")]
|
|
|
|
|
|
- member.balance = compute_balance(this_member_invoices,
|
|
|
+ member.balance = compute_balance(this_member_bills,
|
|
|
this_member_payments)
|
|
|
member.save()
|
|
|
|
|
@@ -502,21 +632,21 @@ def update_accounting_for_member(member):
|
|
|
% (member, float(member.balance)))
|
|
|
|
|
|
|
|
|
-def reconcile_invoices_and_payments(member):
|
|
|
+def reconcile_bills_and_payments(member):
|
|
|
"""
|
|
|
Rapproche des factures et des paiements qui sont actifs (paiement non alloué
|
|
|
ou factures non entièrement payées) automatiquement.
|
|
|
"""
|
|
|
|
|
|
- active_payments, active_invoices = get_active_payment_and_invoices(member)
|
|
|
+ active_payments, active_bills = get_active_payment_and_bills(member)
|
|
|
|
|
|
if active_payments == []:
|
|
|
- accounting_log.info("(No active payment for %s. No invoice/payment "
|
|
|
+ accounting_log.info("(No active payment for %s. No bill/payment "
|
|
|
"reconciliation needed.)."
|
|
|
% member)
|
|
|
return
|
|
|
- elif active_invoices == []:
|
|
|
- accounting_log.info("(No active invoice for %s. No invoice/payment "
|
|
|
+ elif active_bills == []:
|
|
|
+ accounting_log.info("(No active bill for %s. No bill/payment "
|
|
|
"reconciliation needed.)."
|
|
|
% member)
|
|
|
return
|
|
@@ -524,31 +654,31 @@ def reconcile_invoices_and_payments(member):
|
|
|
accounting_log.info("Initiating reconciliation between "
|
|
|
"invoice and payments for %s" % member)
|
|
|
|
|
|
- while active_payments != [] and active_invoices != []:
|
|
|
+ while active_payments != [] and active_bills != []:
|
|
|
|
|
|
- # Only consider the oldest active payment and the oldest active invoice
|
|
|
+ # Only consider the oldest active payment and the oldest active bill
|
|
|
p = active_payments[0]
|
|
|
|
|
|
- # If this payment is to be allocated for a specific invoice...
|
|
|
- if p.invoice:
|
|
|
+ # If this payment is to be allocated for a specific bill...
|
|
|
+ if p.bill:
|
|
|
# Assert that the invoice is still 'active'
|
|
|
- assert p.invoice in active_invoices
|
|
|
- i = p.invoice
|
|
|
+ assert p.bill in active_bills
|
|
|
+ i = p.bill
|
|
|
accounting_log.info("Payment is to be allocated specifically to " \
|
|
|
- "invoice %s" % str(i.number))
|
|
|
+ "bill %s" % str(i.pk))
|
|
|
else:
|
|
|
- i = active_invoices[0]
|
|
|
+ i = active_bills[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)
|
|
|
+ p.allocate_to_bill(i)
|
|
|
|
|
|
- active_payments, active_invoices = get_active_payment_and_invoices(member)
|
|
|
+ active_payments, active_bills = get_active_payment_and_bills(member)
|
|
|
|
|
|
if active_payments == []:
|
|
|
accounting_log.info("No more active payment. Nothing to reconcile anymore.")
|
|
|
- elif active_invoices == []:
|
|
|
+ elif active_bills == []:
|
|
|
accounting_log.info("No more active invoice. Nothing to reconcile anymore.")
|
|
|
return
|
|
|
|
|
@@ -586,31 +716,34 @@ def payment_changed(sender, instance, created, **kwargs):
|
|
|
update_accounting_for_member(instance.member)
|
|
|
|
|
|
|
|
|
-@receiver(post_save, sender=Invoice)
|
|
|
+@receiver(post_save, sender=Bill)
|
|
|
@disable_for_loaddata
|
|
|
-def invoice_changed(sender, instance, created, **kwargs):
|
|
|
+def bill_changed(sender, instance, created, **kwargs):
|
|
|
|
|
|
- if created:
|
|
|
- accounting_log.info("Creating draft invoice %s (Member: %s)."
|
|
|
- % ('DRAFT-{}'.format(instance.pk), instance.member))
|
|
|
- else:
|
|
|
- if not instance.validated:
|
|
|
- accounting_log.info("Updating draft invoice %s (Member: %s)."
|
|
|
- % (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.log_change(created)
|
|
|
+
|
|
|
+@receiver(post_save, sender=MembershipFee)
|
|
|
+@disable_for_loaddata
|
|
|
+def fee_changed(sender, instance, created, **kwargs):
|
|
|
+ if created and instance.member is not None:
|
|
|
+ update_accounting_for_member(instance.member)
|
|
|
+
|
|
|
+@receiver(post_save, sender=Donation)
|
|
|
+@disable_for_loaddata
|
|
|
+def fee_changed(sender, instance, created, **kwargs):
|
|
|
+ if created and instance.member is not None:
|
|
|
+ update_accounting_for_member(instance.member)
|
|
|
|
|
|
@receiver(post_delete, sender=PaymentAllocation)
|
|
|
def paymentallocation_deleted(sender, instance, **kwargs):
|
|
|
|
|
|
- invoice = instance.invoice
|
|
|
+ bill = instance.bill
|
|
|
|
|
|
# Reopen invoice if relevant
|
|
|
- if (invoice.amount_remaining_to_pay() > 0) and (invoice.status == "closed"):
|
|
|
- accounting_log.info("Reopening invoice %s ..." % invoice.number)
|
|
|
- invoice.status = "open"
|
|
|
- invoice.save()
|
|
|
+ if (bill.amount_remaining_to_pay() > 0) and (bill.status == "closed"):
|
|
|
+ accounting_log.info("Reopening bill %s ..." % bill.number)
|
|
|
+ bill.status = "open"
|
|
|
+ bill.save()
|
|
|
|
|
|
|
|
|
@receiver(post_delete, sender=Payment)
|
|
@@ -625,10 +758,10 @@ def payment_deleted(sender, instance, **kwargs):
|
|
|
if member is None:
|
|
|
return
|
|
|
|
|
|
- this_member_invoices = [i for i in member.invoices.filter(validated=True).order_by("date")]
|
|
|
+ this_member_bills = Bill.get_member_validated_bills(member)
|
|
|
this_member_payments = [p for p in member.payments.order_by("date")]
|
|
|
|
|
|
- member.balance = compute_balance(this_member_invoices,
|
|
|
+ member.balance = compute_balance(this_member_bills,
|
|
|
this_member_payments)
|
|
|
member.save()
|
|
|
|