2020-03-20 23:30:49 +00:00
|
|
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
2020-04-22 01:26:45 +00:00
|
|
|
from datetime import datetime
|
2020-03-22 17:27:22 +00:00
|
|
|
|
2020-04-22 01:26:45 +00:00
|
|
|
from django.contrib.auth.models import User
|
2020-03-22 16:29:31 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2020-03-20 23:30:49 +00:00
|
|
|
from django.db import models
|
2020-03-22 16:29:31 +00:00
|
|
|
from django.db.models import Q
|
2020-03-20 23:30:49 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2020-04-22 01:26:45 +00:00
|
|
|
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
|
2020-03-20 23:30:49 +00:00
|
|
|
|
|
|
|
|
2020-03-22 00:22:27 +00:00
|
|
|
class Invoice(models.Model):
|
2020-03-23 20:30:57 +00:00
|
|
|
"""
|
2020-03-24 19:22:15 +00:00
|
|
|
An invoice model that can generates a true invoice.
|
2020-03-23 20:30:57 +00:00
|
|
|
"""
|
|
|
|
|
2020-03-20 23:30:49 +00:00
|
|
|
id = models.PositiveIntegerField(
|
|
|
|
primary_key=True,
|
2020-03-22 00:22:27 +00:00
|
|
|
verbose_name=_("Invoice identifier"),
|
2020-03-20 23:30:49 +00:00
|
|
|
)
|
|
|
|
|
2020-03-20 23:41:55 +00:00
|
|
|
bde = models.CharField(
|
|
|
|
max_length=32,
|
|
|
|
default='Saperlistpopette.png',
|
|
|
|
choices=(
|
|
|
|
('Saperlistpopette.png', 'Saper[list]popette'),
|
|
|
|
('Finalist.png', 'Fina[list]'),
|
|
|
|
('Listorique.png', '[List]orique'),
|
|
|
|
('Satellist.png', 'Satel[list]'),
|
|
|
|
('Monopolist.png', 'Monopo[list]'),
|
|
|
|
('Kataclist.png', 'Katac[list]'),
|
|
|
|
),
|
|
|
|
verbose_name=_("BDE"),
|
|
|
|
)
|
|
|
|
|
2020-03-22 14:24:54 +00:00
|
|
|
object = models.CharField(
|
2020-03-20 23:30:49 +00:00
|
|
|
max_length=255,
|
2020-03-22 14:24:54 +00:00
|
|
|
verbose_name=_("Object"),
|
2020-03-20 23:30:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
description = models.TextField(
|
|
|
|
verbose_name=_("Description")
|
|
|
|
)
|
|
|
|
|
|
|
|
name = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("Name"),
|
|
|
|
)
|
|
|
|
|
|
|
|
address = models.TextField(
|
|
|
|
verbose_name=_("Address"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date = models.DateField(
|
|
|
|
auto_now_add=True,
|
|
|
|
verbose_name=_("Place"),
|
|
|
|
)
|
|
|
|
|
|
|
|
acquitted = models.BooleanField(
|
|
|
|
verbose_name=_("Acquitted"),
|
|
|
|
)
|
|
|
|
|
2020-04-06 08:58:16 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("invoice")
|
|
|
|
verbose_name_plural = _("invoices")
|
|
|
|
|
2020-03-20 23:30:49 +00:00
|
|
|
|
|
|
|
class Product(models.Model):
|
2020-03-23 20:30:57 +00:00
|
|
|
"""
|
2020-03-24 19:22:15 +00:00
|
|
|
Product that appears on an invoice.
|
2020-03-23 20:30:57 +00:00
|
|
|
"""
|
|
|
|
|
2020-03-22 00:22:27 +00:00
|
|
|
invoice = models.ForeignKey(
|
|
|
|
Invoice,
|
2020-03-20 23:30:49 +00:00
|
|
|
on_delete=models.PROTECT,
|
|
|
|
)
|
|
|
|
|
|
|
|
designation = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("Designation"),
|
|
|
|
)
|
|
|
|
|
|
|
|
quantity = models.PositiveIntegerField(
|
|
|
|
verbose_name=_("Quantity")
|
|
|
|
)
|
|
|
|
|
2020-03-24 21:01:02 +00:00
|
|
|
amount = models.IntegerField(
|
2020-03-20 23:30:49 +00:00
|
|
|
verbose_name=_("Unit price")
|
|
|
|
)
|
|
|
|
|
2020-03-21 15:49:18 +00:00
|
|
|
@property
|
|
|
|
def amount_euros(self):
|
|
|
|
return self.amount / 100
|
|
|
|
|
2020-03-20 23:30:49 +00:00
|
|
|
@property
|
|
|
|
def total(self):
|
|
|
|
return self.quantity * self.amount
|
2020-03-21 15:49:18 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def total_euros(self):
|
|
|
|
return self.total / 100
|
2020-03-22 16:29:31 +00:00
|
|
|
|
2020-04-06 08:58:16 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("product")
|
|
|
|
verbose_name_plural = _("products")
|
|
|
|
|
2020-03-22 16:29:31 +00:00
|
|
|
|
2020-03-24 16:06:50 +00:00
|
|
|
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)
|
|
|
|
|
2020-04-06 08:58:16 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("remittance type")
|
|
|
|
verbose_name_plural = _("remittance types")
|
|
|
|
|
2020-03-24 16:06:50 +00:00
|
|
|
|
2020-03-22 16:29:31 +00:00
|
|
|
class Remittance(models.Model):
|
2020-03-23 20:30:57 +00:00
|
|
|
"""
|
|
|
|
Treasurers want to regroup checks or bank transfers in bank remittances.
|
|
|
|
"""
|
|
|
|
|
2020-03-22 17:27:22 +00:00
|
|
|
date = models.DateTimeField(
|
2020-03-22 16:29:31 +00:00
|
|
|
auto_now_add=True,
|
|
|
|
verbose_name=_("Date"),
|
|
|
|
)
|
|
|
|
|
2020-03-24 16:06:50 +00:00
|
|
|
remittance_type = models.ForeignKey(
|
|
|
|
RemittanceType,
|
2020-03-22 16:29:31 +00:00
|
|
|
on_delete=models.PROTECT,
|
|
|
|
verbose_name=_("Type"),
|
|
|
|
)
|
|
|
|
|
|
|
|
comment = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("Comment"),
|
|
|
|
)
|
|
|
|
|
2020-03-22 17:27:22 +00:00
|
|
|
closed = models.BooleanField(
|
|
|
|
default=False,
|
|
|
|
verbose_name=_("Closed"),
|
|
|
|
)
|
|
|
|
|
2020-04-06 08:58:16 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("remittance")
|
|
|
|
verbose_name_plural = _("remittances")
|
|
|
|
|
2020-03-23 20:15:59 +00:00
|
|
|
@property
|
|
|
|
def transactions(self):
|
2020-03-24 19:22:15 +00:00
|
|
|
"""
|
|
|
|
:return: Transactions linked to this remittance.
|
|
|
|
"""
|
|
|
|
if not self.pk:
|
|
|
|
return SpecialTransaction.objects.none()
|
2020-03-23 20:15:59 +00:00
|
|
|
return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self)
|
|
|
|
|
2020-03-23 22:42:37 +00:00
|
|
|
def count(self):
|
2020-03-24 19:22:15 +00:00
|
|
|
"""
|
|
|
|
Linked transactions count.
|
|
|
|
"""
|
2020-03-23 20:15:59 +00:00
|
|
|
return self.transactions.count()
|
2020-03-22 16:29:31 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def amount(self):
|
2020-03-24 19:22:15 +00:00
|
|
|
"""
|
|
|
|
Total amount of the remittance.
|
|
|
|
"""
|
2020-03-23 20:15:59 +00:00
|
|
|
return sum(transaction.total for transaction in self.transactions.all())
|
2020-03-22 16:29:31 +00:00
|
|
|
|
2020-03-24 19:22:15 +00:00
|
|
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
|
|
|
# Check if all transactions have the right type.
|
2020-03-24 16:06:50 +00:00
|
|
|
if self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
|
2020-03-22 16:29:31 +00:00
|
|
|
raise ValidationError("All transactions in a remittance must have the same type")
|
|
|
|
|
2020-03-24 16:06:50 +00:00
|
|
|
return super().save(force_insert, force_update, using, update_fields)
|
2020-03-23 20:15:59 +00:00
|
|
|
|
2020-03-23 22:42:37 +00:00
|
|
|
def __str__(self):
|
|
|
|
return _("Remittance #{:d}: {}").format(self.id, self.comment, )
|
|
|
|
|
2020-03-23 20:15:59 +00:00
|
|
|
|
|
|
|
class SpecialTransactionProxy(models.Model):
|
2020-03-23 20:30:57 +00:00
|
|
|
"""
|
|
|
|
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, that makes what we want.
|
|
|
|
"""
|
|
|
|
|
2020-03-23 20:15:59 +00:00
|
|
|
transaction = models.OneToOneField(
|
|
|
|
SpecialTransaction,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
|
|
|
|
|
|
|
remittance = models.ForeignKey(
|
|
|
|
Remittance,
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
null=True,
|
|
|
|
verbose_name=_("Remittance"),
|
|
|
|
)
|
2020-04-06 08:58:16 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("special transaction proxy")
|
|
|
|
verbose_name_plural = _("special transaction proxies")
|
2020-04-22 01:26:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SogeCredit(models.Model):
|
|
|
|
"""
|
|
|
|
Manage the credits from the Société générale.
|
|
|
|
"""
|
|
|
|
user = models.OneToOneField(
|
|
|
|
User,
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
verbose_name=_("user"),
|
|
|
|
)
|
|
|
|
|
|
|
|
transactions = models.ManyToManyField(
|
|
|
|
MembershipTransaction,
|
|
|
|
related_name="+",
|
|
|
|
verbose_name=_("membership transactions"),
|
|
|
|
)
|
|
|
|
|
|
|
|
credit_transaction = models.OneToOneField(
|
|
|
|
SpecialTransaction,
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
verbose_name=_("credit transaction"),
|
|
|
|
null=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def valid(self):
|
|
|
|
return self.credit_transaction is not None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def amount(self):
|
2020-04-22 01:57:19 +00:00
|
|
|
return sum(transaction.total for transaction in self.transactions.all())
|
2020-04-22 01:26:45 +00:00
|
|
|
|
|
|
|
def invalidate(self):
|
|
|
|
"""
|
|
|
|
Invalidating a Société générale delete the transaction of the bank if it was already created.
|
|
|
|
Treasurers must know what they do, With Great Power Comes Great Responsibility...
|
|
|
|
"""
|
|
|
|
if self.valid:
|
|
|
|
self.credit_transaction.valid = False
|
2020-04-22 01:57:19 +00:00
|
|
|
self.credit_transaction._force_save = True
|
2020-04-22 01:26:45 +00:00
|
|
|
self.credit_transaction.save()
|
2020-04-22 01:57:19 +00:00
|
|
|
self.credit_transaction._force_delete = True
|
2020-04-22 01:26:45 +00:00
|
|
|
self.credit_transaction.delete()
|
|
|
|
self.credit_transaction = None
|
|
|
|
for transaction in self.transactions.all():
|
|
|
|
transaction.valid = False
|
2020-04-22 01:57:19 +00:00
|
|
|
transaction._force_save = True
|
2020-04-22 01:26:45 +00:00
|
|
|
transaction.save()
|
|
|
|
|
|
|
|
def validate(self, force=False):
|
|
|
|
if self.valid and not force:
|
|
|
|
# The credit is already done
|
|
|
|
return
|
|
|
|
|
|
|
|
# First invalidate all transaction and delete the credit if already did (and force mode)
|
|
|
|
self.invalidate()
|
|
|
|
self.credit_transaction = SpecialTransaction.objects.create(
|
|
|
|
source=NoteSpecial.objects.get(special_type="Virement bancaire"),
|
|
|
|
destination=self.user.note,
|
|
|
|
quantity=1,
|
|
|
|
amount=self.amount,
|
|
|
|
reason="Crédit société générale",
|
|
|
|
last_name=self.user.last_name,
|
|
|
|
first_name=self.user.first_name,
|
|
|
|
bank="Société générale",
|
|
|
|
)
|
|
|
|
self.save()
|
|
|
|
|
|
|
|
for transaction in self.transactions.all():
|
|
|
|
transaction.valid = True
|
2020-04-22 01:57:19 +00:00
|
|
|
transaction._force_save = True
|
2020-04-22 01:26:45 +00:00
|
|
|
transaction.created_at = datetime.now()
|
|
|
|
transaction.save()
|
|
|
|
|
|
|
|
def delete(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Deleting a SogeCredit is equivalent to say that the Société générale didn't pay.
|
|
|
|
Treasurers must know what they do, this is difficult to undo this operation.
|
|
|
|
With Great Power Comes Great Responsibility...
|
|
|
|
"""
|
2020-04-22 01:57:19 +00:00
|
|
|
|
|
|
|
total_fee = sum(transaction.total for transaction in self.transactions.all() if not transaction.valid)
|
|
|
|
if self.user.note.balance < total_fee:
|
|
|
|
raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. "
|
|
|
|
"Please ask her/him to credit the note before invalidating this credit."))
|
|
|
|
|
2020-04-22 01:26:45 +00:00
|
|
|
self.invalidate()
|
|
|
|
for transaction in self.transactions.all():
|
2020-04-22 01:57:19 +00:00
|
|
|
transaction._force_save = True
|
2020-04-22 01:26:45 +00:00
|
|
|
transaction.valid = True
|
|
|
|
transaction.created_at = datetime.now()
|
|
|
|
transaction.save()
|
|
|
|
super().delete(**kwargs)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("Credit from the Société générale")
|
|
|
|
verbose_name_plural = _("Credits from the Société générale")
|