mirror of
https://github.com/ae-utbm/sith.git
synced 2025-03-26 23:27:13 +00:00
309 lines
8.7 KiB
Python
309 lines
8.7 KiB
Python
#
|
|
# Copyright 2023 © AE UTBM
|
|
# ae@utbm.fr / ae.info@utbm.fr
|
|
#
|
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
# https://ae.utbm.fr.
|
|
#
|
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
|
#
|
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
#
|
|
#
|
|
|
|
from decimal import Decimal
|
|
|
|
from django.conf import settings
|
|
from django.core import validators
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
from phonenumber_field.modelfields import PhoneNumberField
|
|
|
|
from club.models import Club
|
|
from core.models import SithFile
|
|
|
|
|
|
class CurrencyField(models.DecimalField):
|
|
"""Custom database field used for currency."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs["max_digits"] = 12
|
|
kwargs["decimal_places"] = 2
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def to_python(self, value):
|
|
try:
|
|
return super().to_python(value).quantize(Decimal("0.01"))
|
|
except AttributeError:
|
|
return None
|
|
|
|
|
|
if settings.TESTING:
|
|
from model_bakery import baker
|
|
|
|
baker.generators.add(
|
|
CurrencyField,
|
|
lambda: baker.random_gen.gen_decimal(max_digits=8, decimal_places=2),
|
|
)
|
|
else: # pragma: no cover
|
|
# baker is only used in tests, so we don't need coverage for this part
|
|
pass
|
|
|
|
|
|
# Accounting classes
|
|
|
|
|
|
class Company(models.Model):
|
|
name = models.CharField(_("name"), max_length=60)
|
|
street = models.CharField(_("street"), max_length=60, blank=True)
|
|
city = models.CharField(_("city"), max_length=60, blank=True)
|
|
postcode = models.CharField(_("postcode"), max_length=10, blank=True)
|
|
country = models.CharField(_("country"), max_length=32, blank=True)
|
|
phone = PhoneNumberField(_("phone"), blank=True)
|
|
email = models.EmailField(_("email"), blank=True)
|
|
website = models.CharField(_("website"), max_length=64, blank=True)
|
|
|
|
class Meta:
|
|
verbose_name = _("company")
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class BankAccount(models.Model):
|
|
name = models.CharField(_("name"), max_length=30)
|
|
iban = models.CharField(_("iban"), max_length=255, blank=True)
|
|
number = models.CharField(_("account number"), max_length=255, blank=True)
|
|
club = models.ForeignKey(
|
|
Club,
|
|
related_name="bank_accounts",
|
|
verbose_name=_("club"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Bank account")
|
|
ordering = ["club", "name"]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class ClubAccount(models.Model):
|
|
name = models.CharField(_("name"), max_length=30)
|
|
club = models.ForeignKey(
|
|
Club,
|
|
related_name="club_account",
|
|
verbose_name=_("club"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
bank_account = models.ForeignKey(
|
|
BankAccount,
|
|
related_name="club_accounts",
|
|
verbose_name=_("bank account"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("Club account")
|
|
ordering = ["bank_account", "name"]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class GeneralJournal(models.Model):
|
|
"""Class storing all the operations for a period of time."""
|
|
|
|
start_date = models.DateField(_("start date"))
|
|
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
|
|
name = models.CharField(_("name"), max_length=40)
|
|
closed = models.BooleanField(_("is closed"), default=False)
|
|
club_account = models.ForeignKey(
|
|
ClubAccount,
|
|
related_name="journals",
|
|
null=False,
|
|
verbose_name=_("club account"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
amount = CurrencyField(_("amount"), default=0)
|
|
effective_amount = CurrencyField(_("effective_amount"), default=0)
|
|
|
|
class Meta:
|
|
verbose_name = _("General journal")
|
|
ordering = ["-start_date"]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Operation(models.Model):
|
|
"""An operation is a line in the journal, a debit or a credit."""
|
|
|
|
number = models.IntegerField(_("number"))
|
|
journal = models.ForeignKey(
|
|
GeneralJournal,
|
|
related_name="operations",
|
|
null=False,
|
|
verbose_name=_("journal"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
amount = CurrencyField(_("amount"))
|
|
date = models.DateField(_("date"))
|
|
remark = models.CharField(_("comment"), max_length=128, null=True, blank=True)
|
|
mode = models.CharField(
|
|
_("payment method"),
|
|
max_length=255,
|
|
choices=settings.SITH_ACCOUNTING_PAYMENT_METHOD,
|
|
)
|
|
cheque_number = models.CharField(
|
|
_("cheque number"), max_length=32, default="", null=True, blank=True
|
|
)
|
|
invoice = models.ForeignKey(
|
|
SithFile,
|
|
related_name="operations",
|
|
verbose_name=_("invoice"),
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.CASCADE,
|
|
)
|
|
done = models.BooleanField(_("is done"), default=False)
|
|
simpleaccounting_type = models.ForeignKey(
|
|
"SimplifiedAccountingType",
|
|
related_name="operations",
|
|
verbose_name=_("simple type"),
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.CASCADE,
|
|
)
|
|
accounting_type = models.ForeignKey(
|
|
"AccountingType",
|
|
related_name="operations",
|
|
verbose_name=_("accounting type"),
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.CASCADE,
|
|
)
|
|
label = models.ForeignKey(
|
|
"Label",
|
|
related_name="operations",
|
|
verbose_name=_("label"),
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
)
|
|
target_type = models.CharField(
|
|
_("target type"),
|
|
max_length=10,
|
|
choices=[
|
|
("USER", _("User")),
|
|
("CLUB", _("Club")),
|
|
("ACCOUNT", _("Account")),
|
|
("COMPANY", _("Company")),
|
|
("OTHER", _("Other")),
|
|
],
|
|
)
|
|
target_id = models.IntegerField(_("target id"), null=True, blank=True)
|
|
target_label = models.CharField(
|
|
_("target label"), max_length=32, default="", blank=True
|
|
)
|
|
linked_operation = models.OneToOneField(
|
|
"self",
|
|
related_name="operation_linked_to",
|
|
verbose_name=_("linked operation"),
|
|
null=True,
|
|
blank=True,
|
|
default=None,
|
|
on_delete=models.CASCADE,
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = ("number", "journal")
|
|
ordering = ["-number"]
|
|
|
|
def __str__(self):
|
|
return f"{self.amount} € | {self.date} | {self.accounting_type} | {self.done}"
|
|
|
|
|
|
class AccountingType(models.Model):
|
|
"""Accounting types.
|
|
|
|
Those are numbers used in accounting to classify operations
|
|
"""
|
|
|
|
code = models.CharField(
|
|
_("code"),
|
|
max_length=16,
|
|
validators=[
|
|
validators.RegexValidator(
|
|
r"^[0-9]*$", _("An accounting type code contains only numbers")
|
|
)
|
|
],
|
|
)
|
|
label = models.CharField(_("label"), max_length=128)
|
|
movement_type = models.CharField(
|
|
_("movement type"),
|
|
choices=[
|
|
("CREDIT", _("Credit")),
|
|
("DEBIT", _("Debit")),
|
|
("NEUTRAL", _("Neutral")),
|
|
],
|
|
max_length=12,
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("accounting type")
|
|
ordering = ["movement_type", "code"]
|
|
|
|
def __str__(self):
|
|
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
|
|
|
|
|
|
class SimplifiedAccountingType(models.Model):
|
|
"""Simplified version of `AccountingType`."""
|
|
|
|
label = models.CharField(_("label"), max_length=128)
|
|
accounting_type = models.ForeignKey(
|
|
AccountingType,
|
|
related_name="simplified_types",
|
|
verbose_name=_("simplified accounting types"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _("simplified type")
|
|
ordering = ["accounting_type__movement_type", "accounting_type__code"]
|
|
|
|
def __str__(self):
|
|
return (
|
|
f"{self.get_movement_type_display()} "
|
|
f"- {self.accounting_type.code} - {self.label}"
|
|
)
|
|
|
|
@property
|
|
def movement_type(self):
|
|
return self.accounting_type.movement_type
|
|
|
|
def get_movement_type_display(self):
|
|
return self.accounting_type.get_movement_type_display()
|
|
|
|
|
|
class Label(models.Model):
|
|
"""Label allow a club to sort its operations."""
|
|
|
|
name = models.CharField(_("label"), max_length=64)
|
|
club_account = models.ForeignKey(
|
|
ClubAccount,
|
|
related_name="labels",
|
|
verbose_name=_("club account"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
|
|
class Meta:
|
|
unique_together = ("name", "club_account")
|
|
|
|
def __str__(self):
|
|
return "%s (%s)" % (self.name, self.club_account.name)
|