070752bd01
- Les charges et retraits sur des comptes modifient la balance de la caisse sélectionnée. - Comportement particulier pour le compte LIQ : Pas de charge, pas de retrait. La balance de LIQ n'est jamais modifiée (donc pas d'entrée dans AccountNegative). Les achats sur LIQ modifient la balance de lacaisse.
517 lines
18 KiB
Python
517 lines
18 KiB
Python
from django.db import models
|
|
from django.core.urlresolvers import reverse
|
|
from django.core.exceptions import PermissionDenied, ValidationError
|
|
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 datetime import date, timedelta
|
|
import re
|
|
|
|
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 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(default = False)
|
|
# 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(
|
|
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')):
|
|
return self.balance + self.negative.balance_offset
|
|
return self.balance
|
|
|
|
@property
|
|
def name(self):
|
|
if self.first_name and self.last_name:
|
|
return '%s %s' % (self.first_name, self.last_name)
|
|
elif self.first_name:
|
|
return self.first_name
|
|
else:
|
|
return self.last_name
|
|
|
|
@property
|
|
def is_cash(self):
|
|
return self.trigramme == 'LIQ'
|
|
|
|
@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):
|
|
perms = []
|
|
stop_ope = False
|
|
# Checking is cash account
|
|
if self.is_cash:
|
|
# Yes, so no perms and no stop
|
|
return [], False
|
|
# Checking is frozen account
|
|
if self.is_frozen:
|
|
perms.append('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 = - Settings.OVERDRAFT_AMOUNT()
|
|
# 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 + Settings.OVERDRAFT_DURATION()
|
|
else:
|
|
overdraft_until = timezone.now() + Settings.OVERDRAFT_DURATION()
|
|
# Checking it doesn't break 1 rule
|
|
if new_balance < overdraft_amount or timezone.now() > overdraft_until:
|
|
stop_ope = True
|
|
perms.append('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)
|
|
|
|
# Surcharge de delete
|
|
# Pas de suppression possible
|
|
# Cas à régler plus tard
|
|
def delete(self, *args, **kwargs):
|
|
pass
|
|
|
|
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(
|
|
max_digits = 6, decimal_places = 2,
|
|
blank = True, null = True, default = None)
|
|
authz_overdraft_amount = models.DecimalField(
|
|
max_digits = 6, decimal_places = 2,
|
|
blank = True, null = True, default = None)
|
|
authz_overdraft_until = models.DateTimeField(
|
|
blank = True, null = True, default = None)
|
|
comment = models.CharField(max_length = 255, blank = True)
|
|
|
|
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)
|
|
|
|
class Statement(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(max_digits = 6, decimal_places = 2)
|
|
balance_new = models.DecimalField(max_digits = 6, decimal_places = 2)
|
|
amount_taken = models.DecimalField(max_digits = 6, decimal_places = 2)
|
|
amount_error = models.DecimalField(max_digits = 6, decimal_places = 2)
|
|
at = models.DateTimeField(auto_now_add = True)
|
|
|
|
class ArticleCategory(models.Model):
|
|
name = models.CharField(max_length = 45)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
class Article(models.Model):
|
|
name = models.CharField(max_length = 45)
|
|
is_sold = models.BooleanField(default = True)
|
|
price = models.DecimalField(
|
|
max_digits = 6, decimal_places = 2,
|
|
default = 0)
|
|
stock = models.IntegerField(default = 0)
|
|
category = models.ForeignKey(
|
|
ArticleCategory, on_delete = models.PROTECT,
|
|
related_name = "articles")
|
|
|
|
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 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)
|
|
|
|
class Supplier(models.Model):
|
|
articles = models.ManyToManyField(
|
|
Article,
|
|
through = 'SupplierArticle',
|
|
related_name = "suppliers")
|
|
name = models.CharField(max_length = 45)
|
|
address = models.TextField()
|
|
email = models.EmailField()
|
|
phone = models.CharField(max_length = 10)
|
|
comment = models.TextField()
|
|
|
|
class SupplierArticle(models.Model):
|
|
supplier = models.ForeignKey(
|
|
Supplier, on_delete = models.PROTECT)
|
|
article = models.ForeignKey(
|
|
Article, on_delete = models.PROTECT)
|
|
BOX_TYPE_CHOICES = (
|
|
("caisse", "Caisse"),
|
|
("carton", "Carton"),
|
|
("palette", "Palette"),
|
|
("fût", "Fût"),
|
|
)
|
|
box_type = models.CharField(
|
|
choices = BOX_TYPE_CHOICES,
|
|
max_length = choices_length(BOX_TYPE_CHOICES))
|
|
box_capacity = models.PositiveSmallIntegerField()
|
|
price_HT = models.DecimalField(max_digits = 7, decimal_places = 4)
|
|
TVA = models.DecimalField(max_digits = 4, decimal_places = 2)
|
|
rights = models.DecimalField(max_digits = 7, decimal_places = 4)
|
|
|
|
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)
|
|
|
|
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()
|
|
|
|
class TransferGroup(models.Model):
|
|
at = models.DateTimeField(auto_now_add = True)
|
|
# 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)
|
|
|
|
class OperationGroup(models.Model):
|
|
on_acc = models.ForeignKey(
|
|
Account, on_delete = models.PROTECT,
|
|
related_name = "operations")
|
|
checkout = models.ForeignKey(
|
|
Checkout, on_delete = models.PROTECT,
|
|
related_name = "operations")
|
|
at = models.DateTimeField(auto_now_add = True)
|
|
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)
|
|
|
|
class Operation(models.Model):
|
|
PURCHASE = 'purchase'
|
|
DEPOSIT = 'deposit'
|
|
WITHDRAW = 'withdraw'
|
|
|
|
TYPE_ORDER_CHOICES = (
|
|
(PURCHASE, 'Achat'),
|
|
(DEPOSIT, 'Charge'),
|
|
(WITHDRAW, 'Retrait'),
|
|
)
|
|
|
|
group = models.ForeignKey(
|
|
OperationGroup, on_delete = models.PROTECT,
|
|
related_name = "+")
|
|
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)
|
|
is_checkout = models.BooleanField(default = True)
|
|
# 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)
|
|
|
|
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"),
|
|
)
|
|
|
|
class Settings(models.Model):
|
|
name = models.CharField(
|
|
max_length = 45,
|
|
unique = True)
|
|
value_decimal = models.DecimalField(
|
|
max_digits = 6, decimal_places = 2,
|
|
blank = True, null = True, default = None)
|
|
value_account = models.ForeignKey(
|
|
Account, on_delete = models.PROTECT,
|
|
blank = True, null = True, default = None)
|
|
value_duration = models.DurationField(
|
|
blank = True, null = True, default = None)
|
|
|
|
@staticmethod
|
|
def setting_inst(name):
|
|
return Settings.objects.get(name=name)
|
|
|
|
@staticmethod
|
|
def SUBVENTION_COF():
|
|
try:
|
|
return Settings.setting_inst("SUBVENTION_COF").value_decimal
|
|
except Settings.DoesNotExist:
|
|
return 0
|
|
|
|
@staticmethod
|
|
def ADDCOST_AMOUNT():
|
|
try:
|
|
return Settings.setting_inst("ADDCOST_AMOUNT").value_decimal
|
|
except Settings.DoesNotExist:
|
|
return 0
|
|
|
|
@staticmethod
|
|
def ADDCOST_FOR():
|
|
try:
|
|
return Settings.setting_inst("ADDCOST_FOR").value_account
|
|
except Settings.DoesNotExist:
|
|
return None;
|
|
|
|
@staticmethod
|
|
def OVERDRAFT_DURATION():
|
|
try:
|
|
return Settings.setting_inst("OVERDRAFT_DURATION").value_duration
|
|
except Settings.DoesNotExist:
|
|
return timedelta()
|
|
|
|
@staticmethod
|
|
def OVERDRAFT_AMOUNT():
|
|
try:
|
|
return Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
|
|
except Settings.DoesNotExist:
|
|
return 0
|