From e968f2b12f9dc4ff9ccfbad9a539b77dc32d6ab3 Mon Sep 17 00:00:00 2001 From: chibrac Date: Mon, 26 Jun 2017 19:23:01 +0200 Subject: [PATCH] Gestion du solde en option --- cotisations/forms.py | 14 +++++- cotisations/models.py | 10 +++++ cotisations/urls.py | 1 + cotisations/views.py | 44 ++++++++++++++++++- .../0003_optionaluser_solde_negatif.py | 20 +++++++++ preferences/models.py | 7 ++- users/models.py | 14 +++++- users/templates/users/profil.html | 10 ++++- users/views.py | 5 ++- 9 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 preferences/migrations/0003_optionaluser_solde_negatif.py diff --git a/cotisations/forms.py b/cotisations/forms.py index ac2081fb..d2a86bfd 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -46,10 +46,22 @@ class NewFactureForm(ModelForm): banque = cleaned_data.get("banque") if not paiement: raise forms.ValidationError("Le moyen de paiement est obligatoire") - elif paiement.moyen=="chèque" and not (cheque and banque): + elif paiement.moyen.lower()=="chèque" or paiement.moyen.lower()=="cheque" and not (cheque and banque): raise forms.ValidationError("Le numero de chèque et la banque sont obligatoires") return cleaned_data +class CreditSoldeForm(NewFactureForm): + class Meta(NewFactureForm.Meta): + model = Facture + fields = ['paiement','banque','cheque'] + + def __init__(self, *args, **kwargs): + super(CreditSoldeForm, self).__init__(*args, **kwargs) + self.fields['paiement'].queryset = Paiement.objects.exclude(moyen='solde').exclude(moyen="Solde") + + + montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) + class SelectArticleForm(Form): article = forms.ModelChoiceField(queryset=Article.objects.all(), label="Article", required=True) quantity = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)], required=True) diff --git a/cotisations/models.py b/cotisations/models.py index 8df5c86e..63cf97e2 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -25,8 +25,10 @@ from django.db import models from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from dateutil.relativedelta import relativedelta +from django.forms import ValidationError from django.core.validators import MinValueValidator + class Facture(models.Model): PRETTY_NAME = "Factures émises" @@ -107,6 +109,11 @@ class Article(models.Model): iscotisation = models.BooleanField() duration = models.IntegerField(help_text="Durée exprimée en mois entiers", blank=True, null=True) + + def clean(self): + if self.name.lower() == "solde": + raise ValidationError("Solde est un nom d'article invalide") + def __str__(self): return self.name @@ -126,6 +133,9 @@ class Paiement(models.Model): def __str__(self): return self.moyen + def clean(self): + self.moyen = self.moyen.title() + class Cotisation(models.Model): PRETTY_NAME = "Cotisations" diff --git a/cotisations/urls.py b/cotisations/urls.py index a9f3ef1e..5717b34f 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -30,6 +30,7 @@ urlpatterns = [ url(r'^del_facture/(?P[0-9]+)$', views.del_facture, name='del-facture'), url(r'^facture_pdf/(?P[0-9]+)$', views.facture_pdf, name='facture-pdf'), url(r'^new_facture_pdf/$', views.new_facture_pdf, name='new-facture-pdf'), + url(r'^credit_solde/(?P[0-9]+)$', views.credit_solde, name='credit-solde'), url(r'^add_article/$', views.add_article, name='add-article'), url(r'^edit_article/(?P[0-9]+)$', views.edit_article, name='edit-article'), url(r'^del_article/$', views.del_article, name='del-article'), diff --git a/cotisations/views.py b/cotisations/views.py index 95ceff86..e3ae3dcd 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -38,12 +38,12 @@ from reversion import revisions as reversion from reversion.models import Version from .models import Facture, Article, Vente, Cotisation, Paiement, Banque -from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm, BanqueForm, DelBanqueForm, NewFactureFormPdf, SelectArticleForm +from .forms import NewFactureForm, TrezEditFactureForm, EditFactureForm, ArticleForm, DelArticleForm, PaiementForm, DelPaiementForm, BanqueForm, DelBanqueForm, NewFactureFormPdf, CreditSoldeForm, SelectArticleForm from users.models import User from .tex import render_tex from re2o.settings import ASSO_NAME, ASSO_ADDRESS_LINE1, ASSO_ADDRESS_LINE2, ASSO_SIRET, ASSO_EMAIL, ASSO_PHONE, LOGO_PATH from re2o import settings -from preferences.models import GeneralOption +from preferences.models import OptionalUser, GeneralOption from dateutil.relativedelta import relativedelta from django.utils import timezone @@ -87,6 +87,19 @@ def new_facture(request, userid): articles = article_formset # Si au moins un article est rempli if any(art.cleaned_data for art in articles): + options, created = OptionalUser.objects.get_or_create() + user_solde = options.user_solde + solde_negatif = options.solde_negatif + # Si on paye par solde, que l'option est activée, on vérifie que le négatif n'est pas atteint + if user_solde: + if new_facture.paiement == Paiement.objects.get_or_create(moyen='solde')[0]: + prix_total = 0 + for art_item in articles: + if art_item.cleaned_data: + prix_total += art_item.cleaned_data['article'].prix*art_item.cleaned_data['quantity'] + if float(user.solde) - float(prix_total) < solde_negatif: + messages.error(request, "Le solde est insuffisant pour effectuer l'opération") + return redirect("/users/profil/" + userid) with transaction.atomic(), reversion.create_revision(): new_facture.save() reversion.set_user(request.user) @@ -195,6 +208,33 @@ def del_facture(request, factureid): return redirect("/cotisations/") return form({'objet': facture, 'objet_name': 'facture'}, 'cotisations/delete.html', request) +@login_required +@permission_required('cableur') +def credit_solde(request, userid): + """ Credit ou débit de solde """ + try: + user = User.objects.get(pk=userid) + except User.DoesNotExist: + messages.error(request, u"Utilisateur inexistant" ) + return redirect("/cotisations/") + facture = CreditSoldeForm(request.POST or None) + if facture.is_valid(): + facture_instance = facture.save(commit=False) + with transaction.atomic(), reversion.create_revision(): + facture_instance.user = user + facture_instance.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + new_vente = Vente.objects.create(facture=facture_instance, name="solde", prix=facture.cleaned_data['montant'], iscotisation=False, duration=0, number=1) + with transaction.atomic(), reversion.create_revision(): + new_vente.save() + reversion.set_user(request.user) + reversion.set_comment("Création") + messages.success(request, "Solde modifié") + return redirect("/cotisations/") + return form({'factureform': facture}, 'cotisations/facture.html', request) + + @login_required @permission_required('trésorier') def add_article(request): diff --git a/preferences/migrations/0003_optionaluser_solde_negatif.py b/preferences/migrations/0003_optionaluser_solde_negatif.py new file mode 100644 index 00000000..970b67ee --- /dev/null +++ b/preferences/migrations/0003_optionaluser_solde_negatif.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-06-26 01:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0002_auto_20170625_1923'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='solde_negatif', + field=models.DecimalField(decimal_places=2, default=0, max_digits=5), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 00fa0cb1..13106e05 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -22,13 +22,18 @@ from django.db import models - +from cotisations.models import Paiement class OptionalUser(models.Model): is_tel_mandatory = models.BooleanField(default=True) user_solde = models.BooleanField(default=False) + solde_negatif = models.DecimalField(max_digits=5, decimal_places=2, default=0) gpg_fingerprint = models.BooleanField(default=True) + def clean(self): + if self.user_solde: + Paiement.objects.get_or_create(moyen="Solde") + class OptionalMachine(models.Model): password_machine = models.BooleanField(default=False) max_lambdauser_interfaces = models.IntegerField(default=10) diff --git a/users/models.py b/users/models.py index fc617e65..73cb3b99 100644 --- a/users/models.py +++ b/users/models.py @@ -40,7 +40,7 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager from django.core.validators import MinLengthValidator from topologie.models import Room -from cotisations.models import Cotisation, Facture, Vente +from cotisations.models import Cotisation, Facture, Paiement, Vente from machines.models import Interface, Machine from preferences.models import OptionalUser @@ -312,6 +312,18 @@ class User(AbstractBaseUser): else: return max(self.end_adhesion, self.end_whitelist) + @cached_property + def solde(self): + options, created = OptionalUser.objects.get_or_create() + user_solde = options.user_solde + if user_solde: + solde_object, created=Paiement.objects.get_or_create(moyen='Solde') + somme_debit = Vente.objects.filter(facture__in=Facture.objects.filter(user=self, paiement=solde_object)).aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] or 0 + somme_credit =Vente.objects.filter(facture__in=Facture.objects.filter(user=self), name="solde").aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total'] or 0 + return somme_credit - somme_debit + else: + return 0 + def user_interfaces(self): return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=True)) diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 5478b1c7..99e4aab2 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -81,7 +81,6 @@ with this program; if not, write to the Free Software Foundation, Inc., Commentaire {{ user.comment }} - Date d'inscription {{ user.registered }} @@ -130,6 +129,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% else %} Aucun {% endif %} + + {% if user_solde %} + + Solde + {{ user.solde }} € + + {% endif %}

Machines :

Ajouter une machine

@@ -139,7 +145,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Aucune machine

{% endif %}

Cotisations :

- {% if is_cableur %}

Ajouter une cotisation

{% endif%} + {% if is_cableur %}

Ajouter une cotisation {% if user_solde %} Modifier le solde{% endif%}

{% endif%} {% if facture_list %} {% include "cotisations/aff_cotisations.html" with facture_list=facture_list %} {% else %} diff --git a/users/views.py b/users/views.py index aaf83d42..a5e22f65 100644 --- a/users/views.py +++ b/users/views.py @@ -45,7 +45,7 @@ from cotisations.models import Facture from machines.models import Machine, Interface from users.forms import MassArchiveForm, PassForm, ResetPasswordForm from machines.views import unassign_ips, assign_ips -from preferences.models import GeneralOption +from preferences.models import OptionalUser, GeneralOption from re2o.login import hashNT from re2o.settings import REQ_EXPIRE_STR, EMAIL_FROM, ASSO_NAME, ASSO_EMAIL, SITE_NAME @@ -694,6 +694,8 @@ def profil(request, userid): bans = Ban.objects.filter(user__pseudo=users) whitelists = Whitelist.objects.filter(user__pseudo=users) list_droits = Right.objects.filter(user=users) + options, created = OptionalUser.objects.get_or_create() + user_solde = options.user_solde return render( request, 'users/profil.html', @@ -704,6 +706,7 @@ def profil(request, userid): 'ban_list': bans, 'white_list': whitelists, 'list_droits': list_droits, + 'user_solde': user_solde, } )