# -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function, unicode_literals) from builtins import * from datetime import date, timedelta import re 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 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 django.core.cache import cache from gestion.models import Profile 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): # XXX. change this to "profile" profile = models.OneToOneField(Profile, related_name="account_kfet", on_delete=models.CASCADE) 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) created_at = models.DateTimeField(auto_now_add = True, null = True) # 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 profile et user @property def user(self): return self.profile.user @property def username(self): return self.profile.user.username @property def first_name(self): return self.profile.user.first_name @property def last_name(self): return self.profile.user.last_name @property def email(self): return self.profile.user.email @property def departement(self): return self.profile.departement @property def is_cof(self): return self.profile.cof.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): 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 = Settings.OVERDRAFT_DURATION() overdraft_amount_max = Settings.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 Profile # Args: # - data : datas pour User et Profile # Action: # - Enregistre User, Profile à 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 Profile with data profile = self.profile profile.departement = data.get("departement", profile.departement) profile.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 Profile instance (profile, _) = Profile.objects.get_or_create(user=user) if "login_clipper" in data: profile.login_clipper = data['login_clipper'] if "departement" in data: profile.departement = data['departement'] profile.save() if data: self.profile = profile 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) @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(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) not_count = models.BooleanField(default=False) taken_001 = models.PositiveSmallIntegerField(default=0) taken_002 = models.PositiveSmallIntegerField(default=0) taken_005 = models.PositiveSmallIntegerField(default=0) taken_01 = models.PositiveSmallIntegerField(default=0) taken_02 = models.PositiveSmallIntegerField(default=0) taken_05 = models.PositiveSmallIntegerField(default=0) taken_1 = models.PositiveSmallIntegerField(default=0) taken_2 = models.PositiveSmallIntegerField(default=0) taken_5 = models.PositiveSmallIntegerField(default=0) taken_10 = models.PositiveSmallIntegerField(default=0) taken_20 = models.PositiveSmallIntegerField(default=0) taken_50 = models.PositiveSmallIntegerField(default=0) taken_100 = models.PositiveSmallIntegerField(default=0) taken_200 = models.PositiveSmallIntegerField(default=0) taken_500 = models.PositiveSmallIntegerField(default=0) taken_cheque = models.DecimalField(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(max_length = 45) def __str__(self): return self.name @python_2_unicode_compatible 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") 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), blank = True, null = True, default = None) box_capacity = models.PositiveSmallIntegerField( 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(max_length = 45) address = models.TextField() email = models.EmailField() phone = models.CharField(max_length = 10) comment = models.TextField() 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(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 = "opesgroup") checkout = models.ForeignKey( Checkout, on_delete = models.PROTECT, related_name = "opesgroup") 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' INITIAL = 'initial' TYPE_ORDER_CHOICES = ( (PURCHASE, 'Achat'), (DEPOSIT, 'Charge'), (WITHDRAW, 'Retrait'), (INITIAL, 'Initial'), ) 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) 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"), ('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 Settings(models.Model): name = models.CharField( max_length = 45, unique = True, db_index = 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(): subvention_cof = cache.get('SUBVENTION_COF') if subvention_cof: return subvention_cof try: subvention_cof = Settings.setting_inst("SUBVENTION_COF").value_decimal except Settings.DoesNotExist: subvention_cof = 0 cache.set('SUBVENTION_COF', subvention_cof) return subvention_cof @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(): overdraft_duration = cache.get('OVERDRAFT_DURATION') if overdraft_duration: return overdraft_duration try: overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration except Settings.DoesNotExist: overdraft_duration = timedelta() cache.set('OVERDRAFT_DURATION', overdraft_duration) return overdraft_duration @staticmethod def OVERDRAFT_AMOUNT(): overdraft_amount = cache.get('OVERDRAFT_AMOUNT') if overdraft_amount: return overdraft_amount try: overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal except Settings.DoesNotExist: overdraft_amount = 0 cache.set('OVERDRAFT_AMOUNT', overdraft_amount) return overdraft_amount @staticmethod def CANCEL_DURATION(): cancel_duration = cache.get('CANCEL_DURATION') if cancel_duration: return cancel_duration try: cancel_duration = Settings.setting_inst("CANCEL_DURATION").value_duration except Settings.DoesNotExist: cancel_duration = timedelta() cache.set('CANCEL_DURATION', cancel_duration) return cancel_duration @staticmethod def create_missing(): s, created = Settings.objects.get_or_create(name='SUBVENTION_COF') if created: s.value_decimal = 25 s.save() s, created = Settings.objects.get_or_create(name='ADDCOST_AMOUNT') if created: s.value_decimal = 0.5 s.save() s, created = Settings.objects.get_or_create(name='ADDCOST_FOR') s, created = Settings.objects.get_or_create(name='OVERDRAFT_DURATION') if created: s.value_duration = timedelta(days=1) # 24h s.save() s, created = Settings.objects.get_or_create(name='OVERDRAFT_AMOUNT') if created: s.value_decimal = 20 s.save() s, created = Settings.objects.get_or_create(name='CANCEL_DURATION') if created: s.value_duration = timedelta(minutes=5) # 5min s.save() @staticmethod def empty_cache(): cache.delete_many([ 'SUBVENTION_COF', 'OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT', 'CANCEL_DURATION', 'ADDCOST_AMOUNT', 'ADDCOST_FOR', ]) class GenericTeamToken(models.Model): token = models.CharField(max_length = 50, unique = True)