Commit 932f6470 authored by root's avatar root

Merge branch 'master' into ouverture_des_ports

parents b84f654f 54cb9f46
......@@ -4,3 +4,4 @@ settings_local.py
re2o.png
__pycache__/*
static_files/*
static/logo/*
......@@ -28,23 +28,37 @@ from reversion.admin import VersionAdmin
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
class FactureAdmin(VersionAdmin):
list_display = ('user','paiement','date','valid','control')
"""Class admin d'une facture, tous les champs"""
pass
class VenteAdmin(VersionAdmin):
list_display = ('facture','name','prix','number','iscotisation','duration')
"""Class admin d'une vente, tous les champs (facture related)"""
pass
class ArticleAdmin(VersionAdmin):
list_display = ('name','prix','iscotisation','duration')
"""Class admin d'un article en vente"""
pass
class BanqueAdmin(VersionAdmin):
list_display = ('name',)
"""Class admin de la liste des banques (facture related)"""
pass
class PaiementAdmin(VersionAdmin):
list_display = ('moyen','type_paiement')
"""Class admin d'un moyen de paiement (facture related"""
pass
class CotisationAdmin(VersionAdmin):
list_display = ('vente','date_start','date_end')
"""Class admin d'une cotisation (date de debut et de fin),
Vente related"""
pass
admin.site.register(Facture, FactureAdmin)
admin.site.register(Article, ArticleAdmin)
......
......@@ -19,74 +19,125 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Forms de l'application cotisation de re2o. Dépendance avec les models,
importé par les views.
Permet de créer une nouvelle facture pour un user (NewFactureForm),
et de l'editer (soit l'user avec EditFactureForm,
soit le trésorier avec TrezEdit qui a plus de possibilités que self
notamment sur le controle trésorier)
SelectArticleForm est utilisée lors de la creation d'une facture en
parrallèle de NewFacture pour le choix des articles désirés.
(la vue correspondante est unique)
ArticleForm, BanqueForm, PaiementForm permettent aux admin d'ajouter,
éditer ou supprimer une banque/moyen de paiement ou un article
"""
from __future__ import unicode_literals
from django import forms
from django.forms import ModelForm, Form
from django import forms
from django.core.validators import MinValueValidator
from .models import Article, Paiement, Facture, Banque, Vente
from .models import Article, Paiement, Facture, Banque
class NewFactureForm(ModelForm):
"""Creation d'une facture, moyen de paiement, banque et numero
de cheque"""
def __init__(self, *args, **kwargs):
super(NewFactureForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(NewFactureForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['cheque'].required = False
self.fields['banque'].required = False
self.fields['cheque'].label = 'Numero de chèque'
self.fields['banque'].empty_label = "Non renseigné"
self.fields['paiement'].empty_label = "Séléctionner un moyen de paiement"
self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects.filter(type_paiement=1).first().id
self.fields['paiement'].empty_label = "Séléctionner\
un moyen de paiement"
self.fields['paiement'].widget.attrs['data-cheque'] = Paiement.objects\
.filter(type_paiement=1).first().id
class Meta:
model = Facture
fields = ['paiement','banque','cheque']
fields = ['paiement', 'banque', 'cheque']
def clean(self):
cleaned_data=super(NewFactureForm, self).clean()
cleaned_data = super(NewFactureForm, self).clean()
paiement = cleaned_data.get("paiement")
cheque = cleaned_data.get("cheque")
banque = cleaned_data.get("banque")
if not paiement:
raise forms.ValidationError("Le moyen de paiement est obligatoire.")
raise forms.ValidationError("Le moyen de paiement est obligatoire")
elif paiement.type_paiement == "check" and not (cheque and banque):
raise forms.ValidationError("Le numéro de chèque et la banque sont obligatoires.")
raise forms.ValidationError("Le numéro de chèque et\
la banque sont obligatoires.")
return cleaned_data
class CreditSoldeForm(NewFactureForm):
"""Permet de faire des opérations sur le solde si il est activé"""
class Meta(NewFactureForm.Meta):
model = Facture
fields = ['paiement','banque','cheque']
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")
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)
"""Selection d'un article lors de la creation d'une facture"""
article = forms.ModelChoiceField(
queryset=Article.objects.all(),
label="Article",
required=True
)
quantity = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)],
required=True
)
class NewFactureFormPdf(Form):
article = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Article")
number = forms.IntegerField(label="Quantité", validators=[MinValueValidator(1)])
"""Creation d'un pdf facture par le trésorier"""
article = forms.ModelMultipleChoiceField(
queryset=Article.objects.all(),
label="Article"
)
number = forms.IntegerField(
label="Quantité",
validators=[MinValueValidator(1)]
)
paid = forms.BooleanField(label="Payé", required=False)
dest = forms.CharField(required=True, max_length=255, label="Destinataire")
chambre = forms.CharField(required=False, max_length=10, label="Adresse")
fid = forms.CharField(required=True, max_length=10, label="Numéro de la facture")
fid = forms.CharField(
required=True,
max_length=10,
label="Numéro de la facture"
)
class EditFactureForm(NewFactureForm):
"""Edition d'une facture : moyen de paiement, banque, user parent"""
class Meta(NewFactureForm.Meta):
fields = ['paiement','banque','cheque','user']
fields = ['paiement', 'banque', 'cheque', 'user']
def __init__(self, *args, **kwargs):
super(EditFactureForm, self).__init__(*args, **kwargs)
self.fields['user'].label = 'Adherent'
self.fields['user'].empty_label = "Séléctionner l'adhérent propriétaire"
self.fields['user'].empty_label = "Séléctionner\
l'adhérent propriétaire"
class TrezEditFactureForm(EditFactureForm):
"""Vue pour édition controle trésorier"""
class Meta(EditFactureForm.Meta):
fields = '__all__'
......@@ -97,38 +148,67 @@ class TrezEditFactureForm(EditFactureForm):
class ArticleForm(ModelForm):
"""Creation d'un article. Champs : nom, cotisation, durée"""
class Meta:
model = Article
fields = '__all__'
def __init__(self, *args, **kwargs):
super(ArticleForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = "Désignation de l'article"
class DelArticleForm(Form):
articles = forms.ModelMultipleChoiceField(queryset=Article.objects.all(), label="Articles actuels", widget=forms.CheckboxSelectMultiple)
"""Suppression d'un ou plusieurs articles en vente. Choix
parmis les modèles"""
articles = forms.ModelMultipleChoiceField(
queryset=Article.objects.all(),
label="Articles actuels",
widget=forms.CheckboxSelectMultiple
)
class PaiementForm(ModelForm):
"""Creation d'un moyen de paiement, champ text moyen et type
permettant d'indiquer si il s'agit d'un chèque ou non pour le form"""
class Meta:
model = Paiement
fields = ['moyen', 'type_paiement']
def __init__(self, *args, **kwargs):
super(PaiementForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['moyen'].label = 'Moyen de paiement à ajouter'
self.fields['type_paiement'].label = 'Type de paiement à ajouter'
class DelPaiementForm(Form):
paiements = forms.ModelMultipleChoiceField(queryset=Paiement.objects.all(), label="Moyens de paiement actuels", widget=forms.CheckboxSelectMultiple)
"""Suppression d'un ou plusieurs moyens de paiements, selection
parmis les models"""
paiements = forms.ModelMultipleChoiceField(
queryset=Paiement.objects.all(),
label="Moyens de paiement actuels",
widget=forms.CheckboxSelectMultiple
)
class BanqueForm(ModelForm):
"""Creation d'une banque, field name"""
class Meta:
model = Banque
fields = ['name']
def __init__(self, *args, **kwargs):
super(BanqueForm, self).__init__(*args, **kwargs)
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['name'].label = 'Banque à ajouter'
class DelBanqueForm(Form):
banques = forms.ModelMultipleChoiceField(queryset=Banque.objects.all(), label="Banques actuelles", widget=forms.CheckboxSelectMultiple)
"""Selection d'une ou plusieurs banques, pour suppression"""
banques = forms.ModelMultipleChoiceField(
queryset=Banque.objects.all(),
label="Banques actuelles",
widget=forms.CheckboxSelectMultiple
)
......@@ -20,59 +20,111 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
Definition des models bdd pour les factures et cotisation.
Pièce maitresse : l'ensemble du code intelligent se trouve ici,
dans les clean et save des models ainsi que de leur methodes supplémentaires.
Facture : reliée à un user, elle a un moyen de paiement, une banque (option),
une ou plusieurs ventes
Article : liste des articles en vente, leur prix, etc
Vente : ensemble des ventes effectuées, reliées à une facture (foreignkey)
Banque : liste des banques existantes
Cotisation : objets de cotisation, contenant un début et une fin. Reliées
aux ventes, en onetoone entre une vente et une cotisation.
Crées automatiquement au save des ventes.
Post_save et Post_delete : sychronisation des services et régénération
des services d'accès réseau (ex dhcp) lors de la vente d'une cotisation
par exemple
"""
from __future__ import unicode_literals
from dateutil.relativedelta import relativedelta
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
from django.db.models import Max
from django.utils import timezone
from machines.models import regen
class Facture(models.Model):
""" Définition du modèle des factures. Une facture regroupe une ou
plusieurs ventes, rattachée à un user, et reliée à un moyen de paiement
et si il y a lieu un numero pour les chèques. Possède les valeurs
valides et controle (trésorerie)"""
PRETTY_NAME = "Factures émises"
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
banque = models.ForeignKey('Banque', on_delete=models.PROTECT, blank=True, null=True)
banque = models.ForeignKey(
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True)
cheque = models.CharField(max_length=255, blank=True)
date = models.DateTimeField(auto_now_add=True)
valid = models.BooleanField(default=True)
control = models.BooleanField(default=False)
def prix(self):
prix = Vente.objects.filter(facture=self).aggregate(models.Sum('prix'))['prix__sum']
"""Renvoie le prix brut sans les quantités. Méthode
dépréciée"""
prix = Vente.objects.filter(
facture=self
).aggregate(models.Sum('prix'))['prix__sum']
return prix
def prix_total(self):
return Vente.objects.filter(facture=self).aggregate(total=models.Sum(models.F('prix')*models.F('number'), output_field=models.FloatField()))['total']
"""Prix total : somme des produits prix_unitaire et quantité des
ventes de l'objet"""
return Vente.objects.filter(
facture=self
).aggregate(
total=models.Sum(
models.F('prix')*models.F('number'),
output_field=models.FloatField()
)
)['total']
def name(self):
name = ' - '.join(Vente.objects.filter(facture=self).values_list('name', flat=True))
"""String, somme des name des ventes de self"""
name = ' - '.join(Vente.objects.filter(
facture=self
).values_list('name', flat=True))
return name
def __str__(self):
return str(self.user) + ' ' + str(self.date)
@receiver(post_save, sender=Facture)
def facture_post_save(sender, **kwargs):
"""Post save d'une facture, synchronise l'user ldap"""
facture = kwargs['instance']
user = facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Facture)
def facture_post_delete(sender, **kwargs):
"""Après la suppression d'une facture, on synchronise l'user ldap"""
user = kwargs['instance'].user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class Vente(models.Model):
"""Objet vente, contient une quantité, une facture parente, un nom,
un prix. Peut-être relié à un objet cotisation, via le boolean
iscotisation"""
PRETTY_NAME = "Ventes effectuées"
facture = models.ForeignKey('Facture', on_delete=models.CASCADE)
......@@ -80,44 +132,67 @@ class Vente(models.Model):
name = models.CharField(max_length=255)
prix = models.DecimalField(max_digits=5, decimal_places=2)
iscotisation = models.BooleanField()
duration = models.IntegerField(help_text="Durée exprimée en mois entiers", blank=True, null=True)
duration = models.IntegerField(
help_text="Durée exprimée en mois entiers",
blank=True,
null=True)
def prix_total(self):
"""Renvoie le prix_total de self (nombre*prix)"""
return self.prix*self.number
def update_cotisation(self):
"""Mets à jour l'objet related cotisation de la vente, si
il existe : update la date de fin à partir de la durée de
la vente"""
if hasattr(self, 'cotisation'):
cotisation = self.cotisation
cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number)
cotisation.date_end = cotisation.date_start + relativedelta(
months=self.duration*self.number)
return
def create_cotis(self, date_start=False):
""" Update et crée l'objet cotisation associé à une facture, prend en argument l'user, la facture pour la quantitéi, et l'article pour la durée"""
"""Update et crée l'objet cotisation associé à une facture, prend
en argument l'user, la facture pour la quantitéi, et l'article pour
la durée"""
if not hasattr(self, 'cotisation'):
cotisation=Cotisation(vente=self)
cotisation = Cotisation(vente=self)
if date_start:
end_adhesion = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self.facture.user).exclude(valid=False))).filter(date_start__lt=date_start).aggregate(Max('date_end'))['date_end__max']
end_adhesion = Cotisation.objects.filter(
vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter(
user=self.facture.user
).exclude(valid=False))
).filter(
date_start__lt=date_start
).aggregate(Max('date_end'))['date_end__max']
else:
end_adhesion = self.facture.user.end_adhesion()
date_start = date_start or timezone.now()
end_adhesion = end_adhesion or date_start
date_max = max(end_adhesion, date_start)
cotisation.date_start = date_max
cotisation.date_end = cotisation.date_start + relativedelta(months=self.duration*self.number)
cotisation.date_end = cotisation.date_start + relativedelta(
months=self.duration*self.number
)
return
def save(self, *args, **kwargs):
# On verifie que si iscotisation, duration est présent
if self.iscotisation and not self.duration:
raise ValidationError("Cotisation et durée doivent être présents ensembles")
raise ValidationError("Cotisation et durée doivent être présents\
ensembles")
self.update_cotisation()
super(Vente, self).save(*args, **kwargs)
def __str__(self):
return str(self.name) + ' ' + str(self.facture)
@receiver(post_save, sender=Vente)
def vente_post_save(sender, **kwargs):
"""Post save d'une vente, déclencge la création de l'objet cotisation
si il y a lieu(si iscotisation) """
vente = kwargs['instance']
if hasattr(vente, 'cotisation'):
vente.cotisation.vente = vente
......@@ -128,14 +203,20 @@ def vente_post_save(sender, **kwargs):
user = vente.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Vente)
def vente_post_delete(sender, **kwargs):
"""Après suppression d'une vente, on synchronise l'user ldap (ex
suppression d'une cotisation"""
vente = kwargs['instance']
if vente.iscotisation:
user = vente.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
class Article(models.Model):
"""Liste des articles en vente : prix, nom, et attribut iscotisation
et duree si c'est une cotisation"""
PRETTY_NAME = "Articles en vente"
name = models.CharField(max_length=255, unique=True)
......@@ -154,7 +235,9 @@ class Article(models.Model):
def __str__(self):
return self.name
class Banque(models.Model):
"""Liste des banques"""
PRETTY_NAME = "Banques enregistrées"
name = models.CharField(max_length=255)
......@@ -162,7 +245,9 @@ class Banque(models.Model):
def __str__(self):
return self.name
class Paiement(models.Model):
"""Moyens de paiement"""
PRETTY_NAME = "Moyens de paiement"
PAYMENT_TYPES = (
(0, 'Autre'),
......@@ -179,11 +264,15 @@ class Paiement(models.Model):
self.moyen = self.moyen.title()
def save(self, *args, **kwargs):
"""Un seul type de paiement peut-etre cheque..."""
if Paiement.objects.filter(type_paiement=1).count() > 1:
raise ValidationError("On ne peut avoir plusieurs mode de paiement chèque")
raise ValidationError("On ne peut avoir plusieurs mode de paiement\
chèque")
super(Paiement, self).save(*args, **kwargs)
class Cotisation(models.Model):
"""Objet cotisation, debut et fin, relié en onetoone à une vente"""
PRETTY_NAME = "Cotisations"
vente = models.OneToOneField('Vente', on_delete=models.CASCADE, null=True)
......@@ -193,15 +282,19 @@ class Cotisation(models.Model):
def __str__(self):
return str(self.vente)
@receiver(post_save, sender=Cotisation)
def cotisation_post_save(sender, **kwargs):
"""Après modification d'une cotisation, regeneration des services"""
regen('dns')
regen('dhcp')
regen('mac_ip_list')
regen('mailing')
@receiver(post_delete, sender=Cotisation)
def vente_post_delete(sender, **kwargs):
"""Après suppression d'une vente, régénération des services"""
cotisation = kwargs['instance']
regen('mac_ip_list')
regen('mailing')
......@@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %}
{% load staticfiles%}
{% load massive_bootstrap_form %}
{% block title %}Création et modification de factures{% endblock %}
......@@ -34,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<form class="form" method="post">
{% csrf_token %}
<h3>Editer la facture</h3>
{% bootstrap_form factureform %}
{% massive_bootstrap_form factureform 'user' %}
{{ venteform.management_form }}
<h3>Articles de la facture</h3>
<table class="table table-striped">
......
......@@ -38,18 +38,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ venteform.management_form }}
<!-- TODO: FIXME to include data-type="check" for right option in id_cheque select -->
<h3>Articles de la facture</h3>
<div id="form_set">
<div id="form_set" class="form-group">
{% for form in venteform.forms %}
<div class='product_to_sell'>
<p>
{{ form.as_table }}
</p>
<div class='product_to_sell form-inline'>
Article : &nbsp;
{% bootstrap_form form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-0-article-remove" type="button">
<span class="glyphicon glyphicon-remove"></span>
</button>
</div>
{% endfor %}
</div>
<p>
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un article" id="add_one">
</p>
<p>
Prix total : <span id="total_price">0,00</span>
</p>
......@@ -63,19 +65,23 @@ with this program; if not, write to the Free Software Foundation, Inc.,
prices[{{ article.id|escapejs }}] = {{ article.prix }};
{% endfor %}
var template = `<p>{{ venteform.empty_form.as_table }}</p>`;
var template = `Article : &nbsp;
{% bootstrap_form venteform.empty_form label_class='sr-only' %}
&nbsp;
<button class="btn btn-danger btn-sm"
id="id_form-__prefix__-article-remove" type="button">
<span class="glyphicon glyphicon-remove"></span>
</button>`
function add_article(){
// Index start at 0 => new_index = number of items
var new_index =
document.getElementsByClassName('product_to_sell').length;
document.getElementById('id_form-TOTAL_FORMS').value =
parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1;
document.getElementById('id_form-TOTAL_FORMS').value ++;
var new_article = document.createElement('div');
new_article.className = 'product_to_sell';
new_article.className = 'product_to_sell form-inline';
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('form_set')
.appendChild(new_article);
document.getElementById('form_set').appendChild(new_article);
add_listenner_for_id(new_index);
}
......@@ -106,18 +112,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
.addEventListener("onkeypress", update_price, true);
document.getElementById('id_form-' + i.toString() + '-quantity')
.addEventListener("change", update_price, true);