from datetime import date, timedelta from decimal import Decimal from django import forms from django.conf import settings from django.contrib.auth.models import User from django.core import validators from django.core.exceptions import ValidationError from django.forms import modelformset_factory from django.utils import timezone from django.utils.translation import gettext_lazy as _ from djconfig.forms import ConfigForm from gestioncof.models import CofProfile from kfet.models import ( Account, Article, ArticleCategory, Checkout, CheckoutStatement, Operation, OperationGroup, Supplier, Transfer, TransferGroup, ) from kfet.statistic import SCALE_CLASS_CHOICES from . import KFET_DELETED_TRIGRAMME from .auth import KFET_GENERIC_TRIGRAMME from .auth.forms import UserGroupForm # noqa # ----- # Widgets # ----- class DateTimeWidget(forms.DateTimeInput): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.attrs["format"] = "%Y-%m-%d %H:%M" class Media: css = {"all": ("kfet/vendor/bootstrap/bootstrap-datetimepicker.min.css",)} js = ("kfet/vendor/bootstrap/bootstrap-datetimepicker.min.js",) # ----- # Account forms # ----- def default_promo(): now = date.today() return now.month <= 8 and now.year - 1 or now.year def get_promo_choices(): return [("", "Sans promo")] + [(r, r) for r in range(1980, date.today().year + 1)] class AccountForm(forms.ModelForm): promo = forms.TypedChoiceField( choices=get_promo_choices, coerce=int, empty_value=None, initial=default_promo, required=False, ) # Surcharge pour passer data à Account.save() def save(self, data={}, *args, **kwargs): obj = super().save(commit=False, *args, **kwargs) obj.save(data=data) return obj class Meta: model = Account fields = ["trigramme", "promo", "nickname"] widgets = {"trigramme": forms.TextInput(attrs={"autocomplete": "off"})} class AccountBalanceForm(forms.ModelForm): class Meta: model = Account fields = ["balance"] class AccountTriForm(AccountForm): def clean_trigramme(self): trigramme = self.cleaned_data["trigramme"] return trigramme.upper() class Meta(AccountForm.Meta): fields = ["trigramme"] class AccountNoTriForm(AccountForm): class Meta(AccountForm.Meta): exclude = ["trigramme"] class AccountPwdForm(forms.Form): pwd1 = forms.CharField( label="Mot de passe K-Fêt", required=False, help_text="Le mot de passe doit contenir au moins huit caractères", widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), min_length=8, ) pwd2 = forms.CharField( label="Confirmer le mot de passe", required=False, widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), ) def __init__(self, *args, account=None, **kwargs): super().__init__(*args, **kwargs) self.account = account def clean(self): pwd1 = self.cleaned_data.get("pwd1", "") pwd2 = self.cleaned_data.get("pwd2", "") if pwd1 != pwd2: self.add_error("pwd2", "Les mots de passe doivent être identiques !") super().clean() def save(self, commit=True): password = self.cleaned_data["pwd1"] self.account.change_pwd(password) if commit: self.account.save() return self.account class AccountFrozenForm(forms.ModelForm): class Meta: model = Account fields = ["is_frozen"] class CofForm(forms.ModelForm): def clean_is_cof(self): instance = getattr(self, "instance", None) if instance and instance.pk: return instance.is_cof else: return False class Meta: model = CofProfile fields = ["login_clipper", "is_cof", "departement"] class UserForm(forms.ModelForm): class Meta: model = User fields = ["username", "first_name", "last_name", "email"] help_texts = {"username": ""} class UserRestrictForm(UserForm): class Meta(UserForm.Meta): fields = ["first_name", "last_name"] class UserInfoForm(UserForm): first_name = forms.CharField(label="Prénom", disabled=True) last_name = forms.CharField(label="Nom de famille", disabled=True) class Meta(UserForm.Meta): fields = ["first_name", "last_name"] # ----- # Checkout forms # ----- class CheckoutForm(forms.ModelForm): class Meta: model = Checkout fields = ["name", "valid_from", "valid_to", "balance", "is_protected"] widgets = {"valid_from": DateTimeWidget(), "valid_to": DateTimeWidget()} class CheckoutRestrictForm(CheckoutForm): class Meta(CheckoutForm.Meta): fields = ["name", "valid_from", "valid_to"] class CheckoutStatementCreateForm(forms.ModelForm): balance_001 = forms.IntegerField(min_value=0, initial=0, required=False) balance_002 = forms.IntegerField(min_value=0, initial=0, required=False) balance_005 = forms.IntegerField(min_value=0, initial=0, required=False) balance_01 = forms.IntegerField(min_value=0, initial=0, required=False) balance_02 = forms.IntegerField(min_value=0, initial=0, required=False) balance_05 = forms.IntegerField(min_value=0, initial=0, required=False) balance_1 = forms.IntegerField(min_value=0, initial=0, required=False) balance_2 = forms.IntegerField(min_value=0, initial=0, required=False) balance_5 = forms.IntegerField(min_value=0, initial=0, required=False) balance_10 = forms.IntegerField(min_value=0, initial=0, required=False) balance_20 = forms.IntegerField(min_value=0, initial=0, required=False) balance_50 = forms.IntegerField(min_value=0, initial=0, required=False) balance_100 = forms.IntegerField(min_value=0, initial=0, required=False) balance_200 = forms.IntegerField(min_value=0, initial=0, required=False) balance_500 = forms.IntegerField(min_value=0, initial=0, required=False) class Meta: model = CheckoutStatement exclude = [ "by", "at", "checkout", "amount_error", "amount_taken", "balance_old", "balance_new", ] def clean(self): not_count = self.cleaned_data["not_count"] if not not_count and ( self.cleaned_data["balance_001"] is None or self.cleaned_data["balance_002"] is None or self.cleaned_data["balance_005"] is None or self.cleaned_data["balance_01"] is None or self.cleaned_data["balance_02"] is None or self.cleaned_data["balance_05"] is None or self.cleaned_data["balance_1"] is None or self.cleaned_data["balance_2"] is None or self.cleaned_data["balance_5"] is None or self.cleaned_data["balance_10"] is None or self.cleaned_data["balance_20"] is None or self.cleaned_data["balance_50"] is None or self.cleaned_data["balance_100"] is None or self.cleaned_data["balance_200"] is None or self.cleaned_data["balance_500"] is None ): raise ValidationError( "Y'a un problème. Si tu comptes la caisse, mets au moins des 0 stp." ) super().clean() class CheckoutStatementUpdateForm(forms.ModelForm): class Meta: model = CheckoutStatement exclude = ["by", "at", "checkout", "amount_error", "amount_taken"] # ----- # Category # ----- class CategoryForm(forms.ModelForm): class Meta: model = ArticleCategory fields = ["name", "has_addcost", "has_reduction"] # ----- # Article forms # ----- class ArticleForm(forms.ModelForm): category_new = forms.CharField( label="Créer une catégorie", max_length=45, required=False ) category = forms.ModelChoiceField( label="Catégorie", queryset=ArticleCategory.objects.all(), required=False ) suppliers = forms.ModelMultipleChoiceField( label="Fournisseurs", queryset=Supplier.objects.all(), required=False ) supplier_new = forms.CharField( label="Créer un fournisseur", max_length=45, required=False ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance.pk: self.initial["suppliers"] = self.instance.suppliers.values_list( "pk", flat=True ) def clean(self): category = self.cleaned_data.get("category") category_new = self.cleaned_data.get("category_new") if not category and not category_new: raise ValidationError("Sélectionnez une catégorie ou créez en une") elif not category: category, _ = ArticleCategory.objects.get_or_create(name=category_new) self.cleaned_data["category"] = category super().clean() class Meta: model = Article fields = [ "name", "is_sold", "hidden", "price", "stock", "category", "box_type", "box_capacity", ] class ArticleRestrictForm(ArticleForm): class Meta(ArticleForm.Meta): fields = [ "name", "is_sold", "hidden", "price", "category", "box_type", "box_capacity", ] # ----- # K-Psul forms # ----- class KPsulOperationGroupForm(forms.ModelForm): # FIXME(AD): Use timezone.now instead of timezone.now() to avoid using a # fixed datetime (application boot here). # One may even use: Checkout.objects.is_valid() if changing # to now = timezone.now is ok in 'is_valid' definition. checkout = forms.ModelChoiceField( queryset=Checkout.objects.filter( is_protected=False, valid_from__lte=timezone.now(), valid_to__gte=timezone.now(), ), widget=forms.HiddenInput(), ) on_acc = forms.ModelChoiceField( queryset=Account.objects.exclude( trigramme__in=[KFET_DELETED_TRIGRAMME, KFET_GENERIC_TRIGRAMME] ), widget=forms.HiddenInput(), ) class Meta: model = OperationGroup fields = ["on_acc", "checkout", "comment"] class KPsulAccountForm(forms.ModelForm): class Meta: model = Account fields = ["trigramme"] widgets = { "trigramme": forms.TextInput( attrs={"autocomplete": "off", "spellcheck": "false"} ) } class KPsulCheckoutForm(forms.Form): checkout = forms.ModelChoiceField( queryset=None, widget=forms.Select(attrs={"id": "id_checkout_select"}) ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Create the queryset on form instanciation to use the current time. self.fields["checkout"].queryset = Checkout.objects.is_valid().filter( is_protected=False ) class KPsulOperationForm(forms.ModelForm): article = forms.ModelChoiceField( queryset=Article.objects.select_related("category").all(), required=False, widget=forms.HiddenInput(), ) article_nb = forms.IntegerField( required=False, initial=None, validators=[validators.MinValueValidator(1)], widget=forms.HiddenInput(), ) class Meta: model = Operation fields = ["type", "amount", "article", "article_nb"] widgets = {"type": forms.HiddenInput(), "amount": forms.HiddenInput()} def clean(self): super().clean() type_ope = self.cleaned_data.get("type") amount = self.cleaned_data.get("amount") article = self.cleaned_data.get("article") article_nb = self.cleaned_data.get("article_nb") errors = [] if type_ope and type_ope == Operation.PURCHASE: if not article or article_nb is None or article_nb < 1: errors.append( ValidationError("Un achat nécessite un article et une quantité") ) elif type_ope and type_ope in [Operation.DEPOSIT, Operation.WITHDRAW]: if not amount or article or article_nb: errors.append(ValidationError("Bad request")) else: if type_ope == Operation.DEPOSIT and amount <= 0: errors.append(ValidationError("Charge non positive")) elif type_ope == Operation.WITHDRAW and amount >= 0: errors.append(ValidationError("Retrait non négatif")) self.cleaned_data["article"] = None self.cleaned_data["article_nb"] = None if errors: raise ValidationError(errors) KPsulOperationFormSet = modelformset_factory( Operation, form=KPsulOperationForm, can_delete=True, extra=0, min_num=1, validate_min=True, ) class AddcostForm(forms.Form): trigramme = forms.CharField(required=False) amount = forms.DecimalField( required=False, max_digits=6, decimal_places=2, min_value=Decimal(0) ) def clean(self): trigramme = self.cleaned_data.get("trigramme") if trigramme: try: Account.objects.get(trigramme=trigramme) except Account.DoesNotExist: raise ValidationError("Compte invalide") else: self.cleaned_data["amount"] = 0 super().clean() # ----- # Settings forms # ----- class KFetConfigForm(ConfigForm): kfet_reduction_cof = forms.DecimalField( label="Réduction COF", initial=Decimal("20"), max_digits=6, decimal_places=2, help_text="Réduction, à donner en pourcentage, appliquée lors d'un " "achat par un-e membre du COF sur le montant en euros.", ) kfet_addcost_amount = forms.DecimalField( label="Montant de la majoration (en €)", initial=Decimal("0"), required=False, max_digits=6, decimal_places=2, ) kfet_addcost_for = forms.ModelChoiceField( label="Destinataire de la majoration", initial=None, required=False, help_text="Laissez vide pour désactiver la majoration.", queryset=( Account.objects.select_related("cofprofile", "cofprofile__user").all() ), ) kfet_overdraft_duration = forms.DurationField( label="Durée du découvert autorisé par défaut", initial=timedelta(days=1) ) kfet_overdraft_amount = forms.DecimalField( label="Montant du découvert autorisé par défaut (en €)", initial=Decimal("20"), max_digits=6, decimal_places=2, ) kfet_cancel_duration = forms.DurationField( label="Durée pour annuler une commande sans mot de passe", initial=timedelta(minutes=5), ) class FilterHistoryForm(forms.Form): start = forms.DateTimeField( label=_("De"), widget=DateTimeWidget, required=False, help_text="Limité à {} jours ({} pour les chefs/trez)".format( settings.KFET_HISTORY_DATE_LIMIT.days, settings.KFET_HISTORY_LONG_DATE_LIMIT.days, ), ) end = forms.DateTimeField(label=_("À"), widget=DateTimeWidget, required=False) checkout = forms.ModelChoiceField( label=_("Caisse"), queryset=Checkout.objects.all(), required=False, empty_label=_("Toutes les caisses"), ) account = forms.ModelChoiceField( label=_("Compte"), queryset=Account.objects.all(), required=False, empty_label=_("Tous les comptes"), ) transfers_only = forms.BooleanField(widget=forms.HiddenInput, required=False) opes_only = forms.BooleanField(widget=forms.HiddenInput, required=False) # ----- # Transfer forms # ----- class TransferGroupForm(forms.ModelForm): class Meta: model = TransferGroup fields = ["comment"] class TransferForm(forms.ModelForm): from_acc = forms.ModelChoiceField( queryset=Account.objects.exclude(trigramme__in=["LIQ", "#13", "GNR"]), widget=forms.HiddenInput(), ) to_acc = forms.ModelChoiceField( queryset=Account.objects.exclude(trigramme__in=["LIQ", "#13", "GNR"]), widget=forms.HiddenInput(), ) def clean_amount(self): amount = self.cleaned_data["amount"] if amount <= 0: raise forms.ValidationError("Le montant d'un transfert doit être positif") return amount class Meta: model = Transfer fields = ["from_acc", "to_acc", "amount"] TransferFormSet = modelformset_factory( Transfer, form=TransferForm, min_num=1, validate_min=True, extra=9 ) # ----- # Inventory forms # ----- class InventoryArticleForm(forms.Form): article = forms.ModelChoiceField( queryset=Article.objects.all(), widget=forms.HiddenInput() ) stock_new = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if "initial" in kwargs: self.name = kwargs["initial"]["name"] self.stock_old = kwargs["initial"]["stock_old"] self.category = kwargs["initial"]["category"] self.category_name = kwargs["initial"]["category__name"] self.box_capacity = kwargs["initial"]["box_capacity"] self.is_sold = kwargs["initial"]["is_sold"] # ----- # Order forms # ----- class OrderArticleForm(forms.Form): article = forms.ModelChoiceField( queryset=Article.objects.all(), widget=forms.HiddenInput() ) quantity_ordered = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if "initial" in kwargs: self.name = kwargs["initial"]["name"] self.stock = kwargs["initial"]["stock"] self.category = kwargs["initial"]["category"] self.category_name = kwargs["initial"]["category__name"] self.box_capacity = kwargs["initial"]["box_capacity"] self.v_all = kwargs["initial"]["v_all"] self.v_moy = kwargs["initial"]["v_moy"] self.v_et = kwargs["initial"]["v_et"] self.v_prev = kwargs["initial"]["v_prev"] self.c_rec = kwargs["initial"]["c_rec"] self.is_sold = kwargs["initial"]["is_sold"] class OrderArticleToInventoryForm(forms.Form): article = forms.ModelChoiceField( queryset=Article.objects.all(), widget=forms.HiddenInput() ) price_HT = forms.DecimalField(max_digits=7, decimal_places=4, required=False) TVA = forms.DecimalField(max_digits=7, decimal_places=2, required=False) rights = forms.DecimalField(max_digits=7, decimal_places=4, required=False) quantity_received = forms.IntegerField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if "initial" in kwargs: self.name = kwargs["initial"]["name"] self.category = kwargs["initial"]["category"] self.category_name = kwargs["initial"]["category__name"] self.quantity_ordered = kwargs["initial"]["quantity_ordered"] # ---- # Formulaires pour les statistiques K-Fêt # ---- class StatScaleForm(forms.Form): """Formulaire pour nettoyer les paramètres envoyés aux vues de statistiques K-Fêt. Non destiné à être affiché. """ name = forms.ChoiceField(choices=SCALE_CLASS_CHOICES) begin = forms.DateTimeField(required=False) end = forms.DateTimeField(required=False) n_steps = forms.IntegerField(required=False) last = forms.BooleanField(required=False) class AccountStatForm(forms.Form): """Idem, mais pour la balance d'un compte""" begin_date = forms.DateTimeField(required=False) end_date = forms.DateTimeField(required=False) last_days = forms.IntegerField(required=False)