forked from DGNum/gestioCOF
63fff6ca7c
Pour pouvoir supprimer un compte, on crée un compte dummy qui a pour but de recevoir les objets non supprimables (caisses, transferts/opérations pour statistiques, etc.). Lors de la délétion d'un compte, tout est transféré sur le dummy, qui est créé via migration.
765 lines
25 KiB
Python
765 lines
25 KiB
Python
import re
|
|
from datetime import date
|
|
from functools import reduce
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.core.validators import RegexValidator
|
|
from django.db import models, transaction
|
|
from django.db.models import F
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from gestioncof.models import CofProfile
|
|
|
|
from . import KFET_DELETED_TRIGRAMME
|
|
from .auth import KFET_GENERIC_TRIGRAMME
|
|
from .auth.models import GenericTeamToken # noqa
|
|
from .config import kfet_config
|
|
from .utils import to_ukf
|
|
|
|
|
|
def choices_length(choices):
|
|
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
|
|
|
|
|
|
def default_promo():
|
|
now = date.today()
|
|
return now.month <= 8 and now.year - 1 or now.year
|
|
|
|
|
|
class AccountManager(models.Manager):
|
|
"""Manager for Account Model."""
|
|
|
|
def get_queryset(self):
|
|
"""Always append related data to this Account."""
|
|
return super().get_queryset().select_related("cofprofile__user", "negative")
|
|
|
|
def get_generic(self):
|
|
"""
|
|
Get the kfet generic account instance.
|
|
"""
|
|
return self.get(trigramme=KFET_GENERIC_TRIGRAMME)
|
|
|
|
def get_by_password(self, password):
|
|
"""
|
|
Get a kfet generic account by clear password.
|
|
|
|
Raises Account.DoesNotExist if no Account has this password.
|
|
"""
|
|
from .auth.utils import hash_password
|
|
|
|
if password is None:
|
|
raise self.model.DoesNotExist
|
|
return self.get(password=hash_password(password))
|
|
|
|
|
|
class Account(models.Model):
|
|
objects = AccountManager()
|
|
|
|
cofprofile = models.OneToOneField(
|
|
CofProfile, on_delete=models.PROTECT, related_name="account_kfet"
|
|
)
|
|
trigramme = models.CharField(
|
|
unique=True,
|
|
max_length=3,
|
|
validators=[RegexValidator(regex="^[^a-z]{3}$")],
|
|
db_index=True,
|
|
)
|
|
balance = models.DecimalField(max_digits=6, decimal_places=2, default=0)
|
|
is_frozen = models.BooleanField("est gelé", default=False)
|
|
created_at = models.DateTimeField(default=timezone.now)
|
|
# Optional
|
|
PROMO_CHOICES = [(r, r) for r in range(1980, date.today().year + 1)]
|
|
promo = models.IntegerField(
|
|
choices=PROMO_CHOICES, blank=True, null=True, default=default_promo()
|
|
)
|
|
nickname = models.CharField("surnom(s)", max_length=255, blank=True, default="")
|
|
password = models.CharField(
|
|
max_length=255, unique=True, blank=True, null=True, default=None
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("is_team", "Is part of the team"),
|
|
("manage_perms", "Gérer les permissions K-Fêt"),
|
|
("manage_addcosts", "Gérer les majorations"),
|
|
("edit_balance_account", "Modifier la balance d'un compte"),
|
|
(
|
|
"change_account_password",
|
|
"Modifier le mot de passe d'une personne de l'équipe",
|
|
),
|
|
("special_add_account", "Créer un compte avec une balance initiale"),
|
|
("can_force_close", "Fermer manuellement la K-Fêt"),
|
|
("see_config", "Voir la configuration K-Fêt"),
|
|
("change_config", "Modifier la configuration K-Fêt"),
|
|
)
|
|
|
|
def __str__(self):
|
|
return "%s (%s)" % (self.trigramme, self.name)
|
|
|
|
# Propriétés pour accéder aux attributs de cofprofile et user
|
|
@property
|
|
def user(self):
|
|
return self.cofprofile.user
|
|
|
|
@property
|
|
def username(self):
|
|
return self.cofprofile.user.username
|
|
|
|
@property
|
|
def first_name(self):
|
|
return self.cofprofile.user.first_name
|
|
|
|
@property
|
|
def last_name(self):
|
|
return self.cofprofile.user.last_name
|
|
|
|
@property
|
|
def email(self):
|
|
return self.cofprofile.user.email
|
|
|
|
@property
|
|
def departement(self):
|
|
return self.cofprofile.departement
|
|
|
|
@property
|
|
def is_cof(self):
|
|
return self.cofprofile.is_cof
|
|
|
|
# Propriétés supplémentaires
|
|
@property
|
|
def balance_ukf(self):
|
|
return to_ukf(self.balance, is_cof=self.is_cof)
|
|
|
|
@property
|
|
def real_balance(self):
|
|
if hasattr(self, "negative") and self.negative.balance_offset:
|
|
return self.balance - self.negative.balance_offset
|
|
return self.balance
|
|
|
|
@property
|
|
def name(self):
|
|
return self.user.get_full_name()
|
|
|
|
@property
|
|
def is_cash(self):
|
|
return self.trigramme == "LIQ"
|
|
|
|
@property
|
|
def need_comment(self):
|
|
return self.trigramme == "#13"
|
|
|
|
@property
|
|
def readable(self):
|
|
return self.trigramme != "GNR"
|
|
|
|
@property
|
|
def is_team(self):
|
|
return self.has_perm("kfet.is_team")
|
|
|
|
@staticmethod
|
|
def is_validandfree(trigramme):
|
|
data = {"is_valid": False, "is_free": False}
|
|
pattern = re.compile("^[^a-z]{3}$")
|
|
data["is_valid"] = pattern.match(trigramme) and True or False
|
|
try:
|
|
Account.objects.get(trigramme=trigramme)
|
|
except Account.DoesNotExist:
|
|
data["is_free"] = True
|
|
return data
|
|
|
|
def perms_to_perform_operation(self, amount):
|
|
overdraft_duration_max = kfet_config.overdraft_duration
|
|
overdraft_amount_max = kfet_config.overdraft_amount
|
|
perms = set()
|
|
stop_ope = False
|
|
# Checking is cash account
|
|
if self.is_cash:
|
|
# Yes, so no perms and no stop
|
|
return set(), False
|
|
if self.need_comment:
|
|
perms.add("kfet.perform_commented_operations")
|
|
# Checking is frozen account
|
|
if self.is_frozen:
|
|
perms.add("kfet.override_frozen_protection")
|
|
new_balance = self.balance + amount
|
|
if new_balance < 0 and amount < 0:
|
|
# Retrieving overdraft amount limit
|
|
if (
|
|
hasattr(self, "negative")
|
|
and self.negative.authz_overdraft_amount is not None
|
|
):
|
|
overdraft_amount = -self.negative.authz_overdraft_amount
|
|
else:
|
|
overdraft_amount = -overdraft_amount_max
|
|
# Retrieving overdraft datetime limit
|
|
if (
|
|
hasattr(self, "negative")
|
|
and self.negative.authz_overdraft_until is not None
|
|
):
|
|
overdraft_until = self.negative.authz_overdraft_until
|
|
elif hasattr(self, "negative"):
|
|
overdraft_until = self.negative.start + overdraft_duration_max
|
|
else:
|
|
overdraft_until = timezone.now() + overdraft_duration_max
|
|
# Checking it doesn't break 1 rule
|
|
if new_balance < overdraft_amount or timezone.now() > overdraft_until:
|
|
stop_ope = True
|
|
perms.add("kfet.perform_negative_operations")
|
|
return perms, stop_ope
|
|
|
|
# Surcharge Méthode save() avec gestions de User et CofProfile
|
|
# Args:
|
|
# - data : datas pour User et CofProfile
|
|
# Action:
|
|
# - Enregistre User, CofProfile à partir de "data"
|
|
# - Enregistre Account
|
|
def save(self, data={}, *args, **kwargs):
|
|
|
|
if self.pk and data:
|
|
# Account update
|
|
|
|
# Updating User with data
|
|
user = self.user
|
|
user.first_name = data.get("first_name", user.first_name)
|
|
user.last_name = data.get("last_name", user.last_name)
|
|
user.email = data.get("email", user.email)
|
|
user.save()
|
|
# Updating CofProfile with data
|
|
cof = self.cofprofile
|
|
cof.departement = data.get("departement", cof.departement)
|
|
cof.save()
|
|
elif data:
|
|
# New account
|
|
|
|
# Checking if user has already an account
|
|
username = data.get("username")
|
|
try:
|
|
user = User.objects.get(username=username)
|
|
if hasattr(user.profile, "account_kfet"):
|
|
trigramme = user.profile.account_kfet.trigramme
|
|
raise Account.UserHasAccount(trigramme)
|
|
except User.DoesNotExist:
|
|
pass
|
|
|
|
# Creating or updating User instance
|
|
(user, _) = User.objects.get_or_create(username=username)
|
|
if "first_name" in data:
|
|
user.first_name = data["first_name"]
|
|
if "last_name" in data:
|
|
user.last_name = data["last_name"]
|
|
if "email" in data:
|
|
user.email = data["email"]
|
|
user.save()
|
|
# Creating or updating CofProfile instance
|
|
(cof, _) = CofProfile.objects.get_or_create(user=user)
|
|
if "login_clipper" in data:
|
|
cof.login_clipper = data["login_clipper"]
|
|
if "departement" in data:
|
|
cof.departement = data["departement"]
|
|
cof.save()
|
|
if data:
|
|
self.cofprofile = cof
|
|
super().save(*args, **kwargs)
|
|
|
|
def change_pwd(self, clear_password):
|
|
from .auth.utils import hash_password
|
|
|
|
self.password = hash_password(clear_password)
|
|
|
|
def update_negative(self):
|
|
if self.real_balance < 0:
|
|
if hasattr(self, "negative") and not self.negative.start:
|
|
self.negative.start = timezone.now()
|
|
self.negative.save()
|
|
elif not hasattr(self, "negative"):
|
|
self.negative = AccountNegative.objects.create(
|
|
account=self, start=timezone.now()
|
|
)
|
|
elif hasattr(self, "negative"):
|
|
# self.real_balance >= 0
|
|
balance_offset = self.negative.balance_offset
|
|
if balance_offset:
|
|
(
|
|
Account.objects.filter(pk=self.pk).update(
|
|
balance=F("balance") - balance_offset
|
|
)
|
|
)
|
|
self.refresh_from_db()
|
|
self.negative.delete()
|
|
|
|
class UserHasAccount(Exception):
|
|
def __init__(self, trigramme):
|
|
self.trigramme = trigramme
|
|
|
|
|
|
def get_deleted_account():
|
|
return Account.objects.get(trigramme=KFET_DELETED_TRIGRAMME)
|
|
|
|
|
|
class AccountNegativeManager(models.Manager):
|
|
"""Manager for AccountNegative model."""
|
|
|
|
def get_queryset(self):
|
|
return super().get_queryset().select_related("account__cofprofile__user")
|
|
|
|
|
|
class AccountNegative(models.Model):
|
|
objects = AccountNegativeManager()
|
|
|
|
account = models.OneToOneField(
|
|
Account, on_delete=models.CASCADE, related_name="negative"
|
|
)
|
|
start = models.DateTimeField(blank=True, null=True, default=None)
|
|
balance_offset = models.DecimalField(
|
|
"décalage de balance",
|
|
help_text="Montant non compris dans l'autorisation de négatif",
|
|
max_digits=6,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
authz_overdraft_amount = models.DecimalField(
|
|
"négatif autorisé",
|
|
max_digits=6,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
authz_overdraft_until = models.DateTimeField(
|
|
"expiration du négatif", blank=True, null=True, default=None
|
|
)
|
|
comment = models.CharField("commentaire", max_length=255, blank=True)
|
|
|
|
class Meta:
|
|
permissions = (("view_negs", "Voir la liste des négatifs"),)
|
|
|
|
@property
|
|
def until_default(self):
|
|
return self.start + kfet_config.overdraft_duration
|
|
|
|
|
|
class CheckoutQuerySet(models.QuerySet):
|
|
def is_valid(self):
|
|
now = timezone.now()
|
|
return self.filter(valid_from__lte=now, valid_to__gte=now)
|
|
|
|
|
|
class Checkout(models.Model):
|
|
created_by = models.ForeignKey(
|
|
Account, on_delete=models.SET(get_deleted_account), related_name="+"
|
|
)
|
|
name = models.CharField(max_length=45)
|
|
valid_from = models.DateTimeField()
|
|
valid_to = models.DateTimeField()
|
|
balance = models.DecimalField(max_digits=6, decimal_places=2, default=0)
|
|
is_protected = models.BooleanField(default=False)
|
|
|
|
objects = CheckoutQuerySet.as_manager()
|
|
|
|
def get_absolute_url(self):
|
|
return reverse("kfet.checkout.read", kwargs={"pk": self.pk})
|
|
|
|
class Meta:
|
|
ordering = ["-valid_to"]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def save(self, *args, **kwargs):
|
|
created = self.pk is None
|
|
|
|
ret = super().save(*args, **kwargs)
|
|
|
|
if created:
|
|
self.statements.create(
|
|
amount_taken=0,
|
|
balance_old=self.balance,
|
|
balance_new=self.balance,
|
|
by=self.created_by,
|
|
)
|
|
|
|
return ret
|
|
|
|
|
|
class CheckoutStatement(models.Model):
|
|
by = models.ForeignKey(
|
|
Account, on_delete=models.SET(get_deleted_account), related_name="+"
|
|
)
|
|
checkout = models.ForeignKey(
|
|
Checkout, on_delete=models.CASCADE, related_name="statements"
|
|
)
|
|
balance_old = models.DecimalField(
|
|
"ancienne balance", max_digits=6, decimal_places=2
|
|
)
|
|
balance_new = models.DecimalField(
|
|
"nouvelle balance", max_digits=6, decimal_places=2
|
|
)
|
|
amount_taken = models.DecimalField("montant pris", max_digits=6, decimal_places=2)
|
|
amount_error = models.DecimalField(
|
|
"montant de l'erreur", max_digits=6, decimal_places=2
|
|
)
|
|
at = models.DateTimeField(auto_now_add=True)
|
|
not_count = models.BooleanField("caisse non comptée", default=False)
|
|
|
|
taken_001 = models.PositiveSmallIntegerField("pièces de 1¢", default=0)
|
|
taken_002 = models.PositiveSmallIntegerField("pièces de 2¢", default=0)
|
|
taken_005 = models.PositiveSmallIntegerField("pièces de 5¢", default=0)
|
|
taken_01 = models.PositiveSmallIntegerField("pièces de 10¢", default=0)
|
|
taken_02 = models.PositiveSmallIntegerField("pièces de 20¢", default=0)
|
|
taken_05 = models.PositiveSmallIntegerField("pièces de 50¢", default=0)
|
|
taken_1 = models.PositiveSmallIntegerField("pièces de 1€", default=0)
|
|
taken_2 = models.PositiveSmallIntegerField("pièces de 2€", default=0)
|
|
taken_5 = models.PositiveSmallIntegerField("billets de 5€", default=0)
|
|
taken_10 = models.PositiveSmallIntegerField("billets de 10€", default=0)
|
|
taken_20 = models.PositiveSmallIntegerField("billets de 20€", default=0)
|
|
taken_50 = models.PositiveSmallIntegerField("billets de 50€", default=0)
|
|
taken_100 = models.PositiveSmallIntegerField("billets de 100€", default=0)
|
|
taken_200 = models.PositiveSmallIntegerField("billets de 200€", default=0)
|
|
taken_500 = models.PositiveSmallIntegerField("billets de 500€", default=0)
|
|
taken_cheque = models.DecimalField(
|
|
"montant des chèques", default=0, max_digits=6, decimal_places=2
|
|
)
|
|
|
|
def __str__(self):
|
|
return "%s %s" % (self.checkout, self.at)
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.pk:
|
|
checkout_id = self.checkout_id
|
|
self.balance_old = Checkout.objects.values_list("balance", flat=True).get(
|
|
pk=checkout_id
|
|
)
|
|
if self.not_count:
|
|
self.balance_new = self.balance_old - self.amount_taken
|
|
self.amount_error = self.balance_new + self.amount_taken - self.balance_old
|
|
with transaction.atomic():
|
|
Checkout.objects.filter(pk=checkout_id).update(balance=self.balance_new)
|
|
super().save(*args, **kwargs)
|
|
else:
|
|
self.amount_error = self.balance_new + self.amount_taken - self.balance_old
|
|
# Si on modifie le dernier relevé d'une caisse et que la nouvelle
|
|
# balance est modifiée alors on modifie la balance actuelle de la caisse
|
|
last_statement = (
|
|
CheckoutStatement.objects.filter(checkout=self.checkout)
|
|
.order_by("at")
|
|
.last()
|
|
)
|
|
if (
|
|
last_statement.pk == self.pk
|
|
and last_statement.balance_new != self.balance_new
|
|
):
|
|
Checkout.objects.filter(pk=self.checkout_id).update(
|
|
balance=F("balance") - last_statement.balance_new + self.balance_new
|
|
)
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ArticleCategory(models.Model):
|
|
name = models.CharField("nom", max_length=45)
|
|
has_addcost = models.BooleanField(
|
|
"majorée",
|
|
default=True,
|
|
help_text="Si oui et qu'une majoration "
|
|
"est active, celle-ci sera "
|
|
"appliquée aux articles de "
|
|
"cette catégorie.",
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Article(models.Model):
|
|
name = models.CharField("nom", max_length=45)
|
|
is_sold = models.BooleanField("en vente", default=True)
|
|
hidden = models.BooleanField(
|
|
"caché",
|
|
default=False,
|
|
help_text="Si oui, ne sera pas affiché "
|
|
"au public ; par exemple "
|
|
"sur la carte.",
|
|
)
|
|
price = models.DecimalField("prix", max_digits=6, decimal_places=2, default=0)
|
|
stock = models.IntegerField(default=0)
|
|
category = models.ForeignKey(
|
|
ArticleCategory,
|
|
on_delete=models.PROTECT,
|
|
related_name="articles",
|
|
verbose_name="catégorie",
|
|
)
|
|
BOX_TYPE_CHOICES = (
|
|
("caisse", "caisse"),
|
|
("carton", "carton"),
|
|
("palette", "palette"),
|
|
("fût", "fût"),
|
|
)
|
|
box_type = models.CharField(
|
|
"type de contenant",
|
|
choices=BOX_TYPE_CHOICES,
|
|
max_length=choices_length(BOX_TYPE_CHOICES),
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
box_capacity = models.PositiveSmallIntegerField(
|
|
"capacité du contenant", blank=True, null=True, default=None
|
|
)
|
|
|
|
def __str__(self):
|
|
return "%s - %s" % (self.category.name, self.name)
|
|
|
|
def get_absolute_url(self):
|
|
return reverse("kfet.article.read", kwargs={"pk": self.pk})
|
|
|
|
def price_ukf(self):
|
|
return to_ukf(self.price)
|
|
|
|
|
|
class Inventory(models.Model):
|
|
articles = models.ManyToManyField(
|
|
Article, through="InventoryArticle", related_name="inventories"
|
|
)
|
|
by = models.ForeignKey(
|
|
Account, on_delete=models.SET(get_deleted_account), related_name="+"
|
|
)
|
|
at = models.DateTimeField(auto_now_add=True)
|
|
# Optional
|
|
order = models.OneToOneField(
|
|
"Order",
|
|
on_delete=models.CASCADE,
|
|
related_name="inventory",
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ["-at"]
|
|
permissions = (
|
|
("order_to_inventory", "Générer un inventaire à partir d'une commande"),
|
|
)
|
|
|
|
|
|
class InventoryArticle(models.Model):
|
|
inventory = models.ForeignKey(Inventory, on_delete=models.CASCADE)
|
|
article = models.ForeignKey(Article, on_delete=models.CASCADE)
|
|
stock_old = models.IntegerField()
|
|
stock_new = models.IntegerField()
|
|
stock_error = models.IntegerField(default=0)
|
|
|
|
def save(self, *args, **kwargs):
|
|
# S'il s'agit d'un inventaire provenant d'une livraison, il n'y a pas
|
|
# d'erreur
|
|
if not self.inventory.order:
|
|
self.stock_error = self.stock_new - self.stock_old
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class Supplier(models.Model):
|
|
articles = models.ManyToManyField(
|
|
Article,
|
|
verbose_name=_("articles vendus"),
|
|
through="SupplierArticle",
|
|
related_name="suppliers",
|
|
)
|
|
name = models.CharField(_("nom"), max_length=45)
|
|
address = models.TextField(_("adresse"), blank=True)
|
|
email = models.EmailField(_("adresse mail"), blank=True)
|
|
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
|
|
comment = models.TextField(_("commentaire"), blank=True)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class SupplierArticle(models.Model):
|
|
supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
|
|
article = models.ForeignKey(Article, on_delete=models.CASCADE)
|
|
at = models.DateTimeField(auto_now_add=True)
|
|
price_HT = models.DecimalField(
|
|
max_digits=7, decimal_places=4, blank=True, null=True, default=None
|
|
)
|
|
TVA = models.DecimalField(
|
|
max_digits=4, decimal_places=2, blank=True, null=True, default=None
|
|
)
|
|
rights = models.DecimalField(
|
|
max_digits=7, decimal_places=4, blank=True, null=True, default=None
|
|
)
|
|
|
|
|
|
class Order(models.Model):
|
|
supplier = models.ForeignKey(
|
|
Supplier, on_delete=models.CASCADE, related_name="orders"
|
|
)
|
|
articles = models.ManyToManyField(
|
|
Article, through="OrderArticle", related_name="orders"
|
|
)
|
|
at = models.DateTimeField(auto_now_add=True)
|
|
amount = models.DecimalField(max_digits=6, decimal_places=2, default=0)
|
|
|
|
class Meta:
|
|
ordering = ["-at"]
|
|
|
|
|
|
class OrderArticle(models.Model):
|
|
order = models.ForeignKey(Order, on_delete=models.CASCADE)
|
|
article = models.ForeignKey(Article, on_delete=models.CASCADE)
|
|
quantity_ordered = models.IntegerField()
|
|
quantity_received = models.IntegerField(default=0)
|
|
|
|
|
|
class TransferGroup(models.Model):
|
|
at = models.DateTimeField(default=timezone.now)
|
|
# Optional
|
|
comment = models.CharField(max_length=255, blank=True, default="")
|
|
valid_by = models.ForeignKey(
|
|
Account,
|
|
on_delete=models.SET_NULL,
|
|
related_name="+",
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
|
|
|
|
class Transfer(models.Model):
|
|
group = models.ForeignKey(
|
|
TransferGroup, on_delete=models.PROTECT, related_name="transfers"
|
|
)
|
|
from_acc = models.ForeignKey(
|
|
Account,
|
|
on_delete=models.SET(get_deleted_account),
|
|
related_name="transfers_from",
|
|
)
|
|
to_acc = models.ForeignKey(
|
|
Account, on_delete=models.SET(get_deleted_account), related_name="transfers_to"
|
|
)
|
|
amount = models.DecimalField(max_digits=6, decimal_places=2)
|
|
# Optional
|
|
canceled_by = models.ForeignKey(
|
|
Account,
|
|
on_delete=models.SET(get_deleted_account),
|
|
null=True,
|
|
blank=True,
|
|
default=None,
|
|
related_name="+",
|
|
)
|
|
canceled_at = models.DateTimeField(null=True, blank=True, default=None)
|
|
|
|
def __str__(self):
|
|
return "{} -> {}: {}€".format(self.from_acc, self.to_acc, self.amount)
|
|
|
|
|
|
class OperationGroup(models.Model):
|
|
on_acc = models.ForeignKey(
|
|
Account, on_delete=models.SET(get_deleted_account), related_name="opesgroup"
|
|
)
|
|
checkout = models.ForeignKey(
|
|
Checkout, on_delete=models.PROTECT, related_name="opesgroup"
|
|
)
|
|
at = models.DateTimeField(default=timezone.now)
|
|
amount = models.DecimalField(max_digits=6, decimal_places=2, default=0)
|
|
is_cof = models.BooleanField(default=False)
|
|
# Optional
|
|
comment = models.CharField(max_length=255, blank=True, default="")
|
|
valid_by = models.ForeignKey(
|
|
Account,
|
|
on_delete=models.SET_NULL,
|
|
related_name="+",
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
|
|
def __str__(self):
|
|
return ", ".join(map(str, self.opes.all()))
|
|
|
|
|
|
class Operation(models.Model):
|
|
PURCHASE = "purchase"
|
|
DEPOSIT = "deposit"
|
|
WITHDRAW = "withdraw"
|
|
INITIAL = "initial"
|
|
EDIT = "edit"
|
|
|
|
TYPE_ORDER_CHOICES = (
|
|
(PURCHASE, "Achat"),
|
|
(DEPOSIT, "Charge"),
|
|
(WITHDRAW, "Retrait"),
|
|
(INITIAL, "Initial"),
|
|
(EDIT, "Édition"),
|
|
)
|
|
|
|
group = models.ForeignKey(
|
|
OperationGroup, on_delete=models.PROTECT, related_name="opes"
|
|
)
|
|
type = models.CharField(
|
|
choices=TYPE_ORDER_CHOICES, max_length=choices_length(TYPE_ORDER_CHOICES)
|
|
)
|
|
amount = models.DecimalField(max_digits=6, decimal_places=2, blank=True, default=0)
|
|
# Optional
|
|
article = models.ForeignKey(
|
|
Article,
|
|
on_delete=models.SET_NULL,
|
|
related_name="operations",
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
article_nb = models.PositiveSmallIntegerField(blank=True, null=True, default=None)
|
|
canceled_by = models.ForeignKey(
|
|
Account,
|
|
on_delete=models.SET(get_deleted_account),
|
|
related_name="+",
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
canceled_at = models.DateTimeField(blank=True, null=True, default=None)
|
|
addcost_for = models.ForeignKey(
|
|
Account,
|
|
on_delete=models.SET(get_deleted_account),
|
|
related_name="addcosts",
|
|
blank=True,
|
|
null=True,
|
|
default=None,
|
|
)
|
|
addcost_amount = models.DecimalField(
|
|
max_digits=6, decimal_places=2, blank=True, null=True, default=None
|
|
)
|
|
|
|
class Meta:
|
|
permissions = (
|
|
("perform_deposit", "Effectuer une charge"),
|
|
("perform_negative_operations", "Enregistrer des commandes en négatif"),
|
|
("override_frozen_protection", "Forcer le gel d'un compte"),
|
|
("cancel_old_operations", "Annuler des commandes non récentes"),
|
|
(
|
|
"perform_commented_operations",
|
|
"Enregistrer des commandes avec commentaires",
|
|
),
|
|
)
|
|
|
|
@property
|
|
def is_checkout(self):
|
|
return (
|
|
self.type == Operation.DEPOSIT
|
|
or self.type == Operation.WITHDRAW
|
|
or (self.type == Operation.PURCHASE and self.group.on_acc.is_cash)
|
|
)
|
|
|
|
def __str__(self):
|
|
templates = {
|
|
self.PURCHASE: "{nb} {article.name} ({amount}€)",
|
|
self.DEPOSIT: "charge ({amount}€)",
|
|
self.WITHDRAW: "retrait ({amount}€)",
|
|
self.INITIAL: "initial ({amount}€)",
|
|
self.EDIT: "édition ({amount}€)",
|
|
}
|
|
return templates[self.type].format(
|
|
nb=self.article_nb, article=self.article, amount=self.amount
|
|
)
|