notes-ker-lann/apps/treasury/models.py
Jean-Baptiste Doderlein 121c49a5b6 Add real BD fee
2022-08-30 07:54:39 +00:00

272 lines
7 KiB
Python

# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
from datetime import date
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club, Membership
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
class Invoice(models.Model):
"""
An invoice model that can generates a true invoice.
"""
id = models.PositiveIntegerField(
primary_key=True,
verbose_name=_("Invoice identifier"),
)
bde = models.CharField(
max_length=32,
default='BDE',
choices=(
('BDE', 'BDE'),
('BDA', 'BDA'),
('BDS', 'BDS'),
),
verbose_name=_("BD?"),
)
object = models.CharField(
max_length=255,
verbose_name=_("Object"),
)
description = models.TextField(
verbose_name=_("Description")
)
name = models.CharField(
max_length=255,
verbose_name=_("Name"),
)
address = models.TextField(
verbose_name=_("Address"),
)
date = models.DateField(
default=date.today,
verbose_name=_("Date"),
)
acquitted = models.BooleanField(
verbose_name=_("Acquitted"),
default=False,
)
locked = models.BooleanField(
verbose_name=_("Locked"),
help_text=_("An invoice can't be edited when it is locked."),
default=False,
)
tex = models.TextField(
default="",
verbose_name=_("tex source"),
)
@transaction.atomic
def save(self, *args, **kwargs):
"""
When an invoice is generated, we store the tex source.
The advantage is to never change the template.
Warning: editing this model regenerate the tex source, so be careful.
"""
old_invoice = Invoice.objects.filter(id=self.id)
if old_invoice.exists():
if old_invoice.get().locked and not self._force_save:
raise ValidationError(_("This invoice is locked and can no longer be edited."))
products = self.products.all()
self.place = "Bruz"
self.my_name = f"{self.bde} ENS Rennes"
self.my_address_street = "14 avenue Robert Schumann"
self.my_city = "35170 Bruz"
self.bank_code = 30003
self.desk_code = 3894
self.account_number = 37280662
self.rib_key = 14
self.bic = "SOGEFRPP"
# Fill the template with the information
self.tex = render_to_string("treasury/invoice_sample.tex", dict(obj=self, products=products))
return super().save(*args, **kwargs)
class Meta:
verbose_name = _("invoice")
verbose_name_plural = _("invoices")
def __str__(self):
return _("Invoice #{id}").format(id=self.id)
class Product(models.Model):
"""
Product that appears on an invoice.
"""
invoice = models.ForeignKey(
Invoice,
on_delete=models.CASCADE,
related_name="products",
verbose_name=_("invoice"),
)
designation = models.CharField(
max_length=255,
verbose_name=_("Designation"),
)
quantity = models.DecimalField(
decimal_places=2,
max_digits=7,
verbose_name=_("Quantity"),
validators=[MinValueValidator(0)],
)
amount = models.IntegerField(
verbose_name=_("Unit price"),
)
@property
def amount_euros(self):
return "{:.2f}".format(self.amount / 100)
@property
def total(self):
return self.quantity * self.amount
@property
def total_euros(self):
return "{:.2f}".format(self.total / 100)
class Meta:
verbose_name = _("product")
verbose_name_plural = _("products")
def __str__(self):
return f"{self.designation} ({self.invoice})"
class RemittanceType(models.Model):
"""
Store what kind of remittances can be stored.
"""
note = models.OneToOneField(
NoteSpecial,
on_delete=models.CASCADE,
)
def __str__(self):
return str(self.note)
class Meta:
verbose_name = _("remittance type")
verbose_name_plural = _("remittance types")
class Remittance(models.Model):
"""
Treasurers want to regroup checks or bank transfers in bank remittances.
"""
date = models.DateTimeField(
default=timezone.now,
verbose_name=_("Date"),
)
remittance_type = models.ForeignKey(
RemittanceType,
on_delete=models.PROTECT,
verbose_name=_("Type"),
)
comment = models.CharField(
max_length=255,
verbose_name=_("Comment"),
)
closed = models.BooleanField(
default=False,
verbose_name=_("Closed"),
)
class Meta:
verbose_name = _("remittance")
verbose_name_plural = _("remittances")
@property
def transactions(self):
"""
:return: Transactions linked to this remittance.
"""
if not self.pk:
return SpecialTransaction.objects.none()
return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self)
def count(self):
"""
Linked transactions count.
"""
return self.transactions.count()
@property
def amount(self):
"""
Total amount of the remittance.
"""
return sum(transaction.total for transaction in self.transactions.all())
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# Check if all transactions have the right type.
if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
raise ValidationError("All transactions in a remittance must have the same type")
return super().save(force_insert, force_update, using, update_fields)
def __str__(self):
return _("Remittance #{:d}: {}").format(self.id, self.comment, )
class SpecialTransactionProxy(models.Model):
"""
In order to keep modularity, we don't that the Note app depends on the treasury app.
That's why we create a proxy in this app, to link special transactions and remittances.
If it isn't very clean, it does what we want.
"""
transaction = models.OneToOneField(
SpecialTransaction,
on_delete=models.CASCADE,
)
remittance = models.ForeignKey(
Remittance,
on_delete=models.PROTECT,
null=True,
related_name="transaction_proxies",
verbose_name=_("Remittance"),
)
class Meta:
verbose_name = _("special transaction proxy")
verbose_name_plural = _("special transaction proxies")
def __str__(self):
return str(self.transaction)