kpsul/kfet/models.py
2017-04-13 15:48:13 +02:00

657 lines
24 KiB
Python

# -*- coding: utf-8 -*-
from django.db import models
from django.core.urlresolvers import reverse
from django.core.validators import RegexValidator
from django.contrib.auth.models import User
from gestioncof.models import CofProfile
from django.utils.six.moves import reduce
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.db import transaction
from django.db.models import F
from datetime import date
import re
import hashlib
from kfet.config import kfet_config
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
@python_2_unicode_compatible
class Account(models.Model):
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)
def __str__(self):
return '%s (%s)' % (self.trigramme, self.name)
# Propriétés pour accéder aux attributs de user et 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 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'
@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 = 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(Account, self).save(*args, **kwargs)
def change_pwd(self, pwd):
pwd_sha256 = hashlib.sha256(pwd.encode('utf-8'))\
.hexdigest()
self.password = pwd_sha256
# Surcharge de delete
# Pas de suppression possible
# Cas à régler plus tard
def delete(self, *args, **kwargs):
pass
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
class AccountNegative(models.Model):
account = models.OneToOneField(
Account, on_delete = models.PROTECT,
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)
@python_2_unicode_compatible
class Checkout(models.Model):
created_by = models.ForeignKey(
Account, on_delete = models.PROTECT,
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)
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
class CheckoutTransfer(models.Model):
from_checkout = models.ForeignKey(
Checkout, on_delete = models.PROTECT,
related_name = "transfers_from")
to_checkout = models.ForeignKey(
Checkout, on_delete = models.PROTECT,
related_name = "transfers_to")
amount = models.DecimalField(
max_digits = 6, decimal_places = 2)
@python_2_unicode_compatible
class CheckoutStatement(models.Model):
by = models.ForeignKey(
Account, on_delete = models.PROTECT,
related_name = "+")
checkout = models.ForeignKey(
Checkout, on_delete = models.PROTECT,
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(CheckoutStatement, self).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(CheckoutStatement, self).save(*args, **kwargs)
@python_2_unicode_compatible
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
@python_2_unicode_compatible
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})
class ArticleRule(models.Model):
article_on = models.OneToOneField(
Article, on_delete = models.PROTECT,
related_name = "rule_on")
article_to = models.OneToOneField(
Article, on_delete = models.PROTECT,
related_name = "rule_to")
ratio = models.PositiveSmallIntegerField()
class Inventory(models.Model):
articles = models.ManyToManyField(
Article,
through = 'InventoryArticle',
related_name = "inventories")
by = models.ForeignKey(
Account, on_delete = models.PROTECT,
related_name = "+")
at = models.DateTimeField(auto_now_add = True)
# Optional
order = models.OneToOneField(
'Order', on_delete = models.PROTECT,
related_name = "inventory",
blank = True, null = True, default = None)
class Meta:
ordering = ['-at']
class InventoryArticle(models.Model):
inventory = models.ForeignKey(
Inventory, on_delete = models.PROTECT)
article = models.ForeignKey(
Article, on_delete = models.PROTECT)
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(InventoryArticle, self).save(*args, **kwargs)
@python_2_unicode_compatible
class Supplier(models.Model):
articles = models.ManyToManyField(
Article,
through = 'SupplierArticle',
related_name = "suppliers")
name = models.CharField("nom", max_length = 45)
address = models.TextField("adresse")
email = models.EmailField("adresse mail")
phone = models.CharField("téléphone", max_length = 10)
comment = models.TextField("commentaire")
def __str__(self):
return self.name
class SupplierArticle(models.Model):
supplier = models.ForeignKey(
Supplier, on_delete = models.PROTECT)
article = models.ForeignKey(
Article, on_delete = models.PROTECT)
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.PROTECT,
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.PROTECT)
article = models.ForeignKey(
Article, on_delete = models.PROTECT)
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.PROTECT,
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.PROTECT,
related_name="transfers_from")
to_acc = models.ForeignKey(
Account, on_delete=models.PROTECT,
related_name="transfers_to")
amount = models.DecimalField(max_digits=6, decimal_places=2)
# Optional
canceled_by = models.ForeignKey(
Account, on_delete=models.PROTECT,
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.PROTECT,
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.PROTECT,
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.PROTECT,
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.PROTECT,
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.PROTECT,
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)
@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)
class GlobalPermissions(models.Model):
class Meta:
managed = False
permissions = (
('is_team', 'Is part of the team'),
('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'),
('manage_perms', 'Gérer les permissions K-Fêt'),
('manage_addcosts', 'Gérer les majorations'),
('perform_commented_operations', 'Enregistrer des commandes avec commentaires'),
('view_negs', 'Voir la liste des négatifs'),
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
('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")
)
class GenericTeamToken(models.Model):
token = models.CharField(max_length = 50, unique = True)