8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-07-04 05:04:06 +00:00
re2o/cotisations/models.py

620 lines
22 KiB
Python
Raw Normal View History

# -*- mode: python; coding: utf-8 -*-
2017-01-15 23:01:18 +00:00
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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.
2017-10-13 21:15:07 +00:00
"""
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
"""
# TODO : translate docstring to English
2017-01-15 23:01:18 +00:00
from __future__ import unicode_literals
2017-10-13 21:15:07 +00:00
from dateutil.relativedelta import relativedelta
2016-07-02 13:58:50 +00:00
from django.db import models
2017-10-29 17:48:30 +00:00
from django.db.models import Q
2016-07-25 22:13:38 +00:00
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
2017-06-26 17:23:01 +00:00
from django.forms import ValidationError
from django.core.validators import MinValueValidator
2017-09-27 13:40:28 +00:00
from django.db.models import Max
from django.utils import timezone
from machines.models import regen
from django.utils.translation import ugettext as _
from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin
# TODO : change facture to invoice
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
2017-10-13 02:07:56 +00:00
""" 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)"""
# TODO : translate docstrign to English
2016-07-02 13:58:50 +00:00
user = models.ForeignKey('users.User', on_delete=models.PROTECT)
# TODO : change paiement to payment
2016-07-02 13:58:50 +00:00
paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
# TODO : change banque to bank
2017-10-13 02:07:56 +00:00
banque = models.ForeignKey(
2017-10-13 21:15:07 +00:00
'Banque',
on_delete=models.PROTECT,
blank=True,
null=True
)
# TODO : maybe change to cheque nummber because not evident
cheque = models.CharField(
max_length=255,
blank=True,
verbose_name=_("Cheque number")
)
date = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Date")
)
# TODO : change name to validity for clarity
valid = models.BooleanField(
default=True,
verbose_name=_("Validated")
)
# TODO : changed name to controlled for clarity
control = models.BooleanField(
default=False,
verbose_name=_("Controlled")
)
class Meta:
abstract = False
permissions = (
# TODO : change facture to invoice
('change_facture_control', _("Can change the \"controlled\" state")),
# TODO : seems more likely to be call create_facture_pdf or create_invoice_pdf
('change_facture_pdf', _("Can create a custom PDF invoice")),
('view_facture', _("Can see an invoice's details")),
('change_all_facture', _("Can edit all the previous invoices")),
)
verbose_name = _("Invoice")
verbose_name_plural = _("Invoices")
2018-04-02 01:52:15 +00:00
def linked_objects(self):
"""Return linked objects : machine and domain.
Usefull in history display"""
return self.vente_set.all()
# TODO : change prix to price
def prix(self):
2017-10-13 21:15:07 +00:00
"""Renvoie le prix brut sans les quantités. Méthode
dépréciée"""
# TODO : translate docstring to English
# TODO : change prix to price
2017-10-13 02:07:56 +00:00
prix = Vente.objects.filter(
2017-10-13 21:15:07 +00:00
facture=self
).aggregate(models.Sum('prix'))['prix__sum']
return prix
# TODO : change prix to price
2016-07-11 22:05:07 +00:00
def prix_total(self):
2017-10-13 02:07:56 +00:00
"""Prix total : somme des produits prix_unitaire et quantité des
ventes de l'objet"""
# TODO : translate docstrign to English
# TODO : change Vente to somethingelse
2017-10-13 02:07:56 +00:00
return Vente.objects.filter(
2017-10-13 21:15:07 +00:00
facture=self
).aggregate(
2017-10-13 02:07:56 +00:00
total=models.Sum(
models.F('prix')*models.F('number'),
output_field=models.FloatField()
2017-10-13 21:15:07 +00:00
)
)['total']
2016-07-11 22:05:07 +00:00
def name(self):
2017-10-13 02:07:56 +00:00
"""String, somme des name des ventes de self"""
# TODO : translate docstring to English
2017-10-13 02:07:56 +00:00
name = ' - '.join(Vente.objects.filter(
facture=self
).values_list('name', flat=True))
return name
def can_edit(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.change_facture'):
return False, _("You don't have the right to edit an invoice.")
elif not user_request.has_perm('cotisations.change_all_facture') and not self.user.can_edit(user_request, *args, **kwargs)[0]:
return False, _("You don't have the right to edit this user's invoices.")
2017-12-31 19:13:41 +00:00
elif not user_request.has_perm('cotisations.change_all_facture') and\
(self.control or not self.valid):
return False, _("You don't have the right to edit an invoice already controlled or invalidated.")
else:
return True, None
def can_delete(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.delete_facture'):
return False, _("You don't have the right to delete an invoice.")
if not self.user.can_edit(user_request, *args, **kwargs)[0]:
return False, _("You don't have the right to delete this user's invoices.")
if self.control or not self.valid:
return False, _("You don't have the right to delete an invoice already controlled or invalidated.")
else:
return True, None
def can_view(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.view_facture') and\
self.user != user_request:
return False, _("You don't have the right to see someone else's invoices history.")
2017-12-27 20:09:00 +00:00
elif not self.valid:
return False, _("The invoice has been invalidated.")
else:
return True, None
2017-12-29 18:43:44 +00:00
@staticmethod
def can_change_control(user_request, *args, **kwargs):
return user_request.has_perm('cotisations.change_facture_control'), _("You don't have the right to edit the \"controlled\" state.")
2017-12-27 20:09:00 +00:00
2017-12-29 18:43:44 +00:00
@staticmethod
def can_change_pdf(user_request, *args, **kwargs):
return user_request.has_perm('cotisations.change_facture_pdf'), _("You don't have the right to edit an invoice.")
2017-12-27 20:09:00 +00:00
def __init__(self, *args, **kwargs):
super(Facture, self).__init__(*args, **kwargs)
self.field_permissions = {
'control' : self.can_change_control,
}
def __str__(self):
2017-01-08 12:41:01 +00:00
return str(self.user) + ' ' + str(self.date)
2017-10-13 02:07:56 +00:00
2016-07-25 22:13:38 +00:00
@receiver(post_save, sender=Facture)
def facture_post_save(sender, **kwargs):
2017-10-13 02:07:56 +00:00
"""Post save d'une facture, synchronise l'user ldap"""
# TODO : translate docstrign into English
2016-07-25 22:13:38 +00:00
facture = kwargs['instance']
user = facture.user
2016-11-20 15:53:59 +00:00
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
2016-07-25 22:13:38 +00:00
2017-10-13 02:07:56 +00:00
2016-07-25 22:13:38 +00:00
@receiver(post_delete, sender=Facture)
def facture_post_delete(sender, **kwargs):
2017-10-13 21:15:07 +00:00
"""Après la suppression d'une facture, on synchronise l'user ldap"""
# TODO : translate docstring into English
2016-07-25 22:13:38 +00:00
user = kwargs['instance'].user
2016-11-20 15:53:59 +00:00
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
2016-07-25 22:13:38 +00:00
2017-10-13 02:07:56 +00:00
# TODO : change Vente to Purchase
class Vente(RevMixin, AclMixin, models.Model):
2017-10-13 02:07:56 +00:00
"""Objet vente, contient une quantité, une facture parente, un nom,
un prix. Peut-être relié à un objet cotisation, via le boolean
iscotisation"""
# TODO : translate docstring into English
# TODO : change this to English
COTISATION_TYPE = (
('Connexion', _("Connexion")),
('Adhesion', _("Membership")),
('All', _("Both of them")),
)
# TODO : change facture to invoice
facture = models.ForeignKey(
'Facture',
on_delete=models.CASCADE,
verbose_name=_("Invoice")
)
# TODO : change number to amount for clarity
number = models.IntegerField(
validators=[MinValueValidator(1)],
verbose_name=_("Amount")
)
# TODO : change this field for a ForeinKey to Article
name = models.CharField(
max_length=255,
verbose_name=_("Article")
)
# TODO : change prix to price
# TODO : this field is not needed if you use Article ForeignKey
prix = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name=_("Price"))
# TODO : this field is not needed if you use Article ForeignKey
duration = models.PositiveIntegerField(
2017-10-13 21:15:07 +00:00
blank=True,
null=True,
verbose_name=_("Duration (in whole month)")
)
# TODO : this field is not needed if you use Article ForeignKey
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
blank=True,
null=True,
max_length=255,
verbose_name=_("Type of cotisation")
)
2016-07-02 13:58:50 +00:00
class Meta:
permissions = (
('view_vente', _("Can see a purchase's details")),
('change_all_vente', _("Can edit all the previous purchases")),
)
verbose_name = _("Purchase")
verbose_name_plural = _("Purchases")
# TODO : change prix_total to total_price
def prix_total(self):
2017-10-13 02:07:56 +00:00
"""Renvoie le prix_total de self (nombre*prix)"""
# TODO : translate docstring to english
return self.prix*self.number
def update_cotisation(self):
2017-10-13 21:15:07 +00:00
"""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"""
# TODO : translate docstring to English
if hasattr(self, 'cotisation'):
cotisation = self.cotisation
2017-10-13 02:07:56 +00:00
cotisation.date_end = cotisation.date_start + relativedelta(
2017-10-13 21:15:07 +00:00
months=self.duration*self.number)
return
def create_cotis(self, date_start=False):
2017-10-13 02:07:56 +00:00
"""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"""
# TODO : translate docstring to English
if not hasattr(self, 'cotisation') and self.type_cotisation:
2017-10-13 02:07:56 +00:00
cotisation = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation
if date_start:
2017-10-29 17:48:30 +00:00
end_cotisation = Cotisation.objects.filter(
2017-10-13 21:15:07 +00:00
vente__in=Vente.objects.filter(
facture__in=Facture.objects.filter(
user=self.facture.user
).exclude(valid=False))
).filter(Q(type_cotisation='All') | Q(type_cotisation=self.type_cotisation)
2017-10-13 21:15:07 +00:00
).filter(
2017-10-13 02:07:56 +00:00
date_start__lt=date_start
2017-10-13 21:15:07 +00:00
).aggregate(Max('date_end'))['date_end__max']
elif self.type_cotisation=="Adhesion":
end_cotisation = self.facture.user.end_adhesion()
else:
end_cotisation = self.facture.user.end_connexion()
date_start = date_start or timezone.now()
end_cotisation = end_cotisation or date_start
date_max = max(end_cotisation, date_start)
cotisation.date_start = date_max
2017-10-13 02:07:56 +00:00
cotisation.date_end = cotisation.date_start + relativedelta(
2017-10-13 21:15:07 +00:00
months=self.duration*self.number
)
return
def save(self, *args, **kwargs):
# TODO : ecrire une docstring
# On verifie que si iscotisation, duration est présent
if self.type_cotisation and not self.duration:
raise ValidationError(
_("A cotisation should always have a duration.")
)
self.update_cotisation()
super(Vente, self).save(*args, **kwargs)
def can_edit(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.change_vente'):
return False, _("You don't have the right to edit the purchases.")
elif not user_request.has_perm('cotisations.change_all_facture') and not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
return False, _("You don't have the right to edit this user's purchases.")
2017-12-31 19:13:41 +00:00
elif not user_request.has_perm('cotisations.change_all_vente') and\
2017-12-27 20:09:00 +00:00
(self.facture.control or not self.facture.valid):
return False, _("You don't have the right to edit a purchase already controlled or invalidated.")
2017-12-27 20:09:00 +00:00
else:
return True, None
def can_delete(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.delete_vente'):
return False, _("You don't have the right to delete a purchase.")
if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
return False, _("You don't have the right to delete this user's purchases.")
2017-12-27 20:09:00 +00:00
if self.facture.control or not self.facture.valid:
return False, _("You don't have the right to delete a purchase already controlled or invalidated.")
2017-12-27 20:09:00 +00:00
else:
return True, None
def can_view(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.view_vente') and\
2017-12-27 20:09:00 +00:00
self.facture.user != user_request:
return False, _("You don't have the right to see someone else's purchase history.")
2017-12-27 20:09:00 +00:00
else:
return True, None
2016-07-02 13:58:50 +00:00
def __str__(self):
return str(self.name) + ' ' + str(self.facture)
2016-07-02 13:58:50 +00:00
2017-10-13 02:07:56 +00:00
# TODO : change vente to purchase
2016-07-25 22:13:38 +00:00
@receiver(post_save, sender=Vente)
def vente_post_save(sender, **kwargs):
2017-10-13 02:07:56 +00:00
"""Post save d'une vente, déclencge la création de l'objet cotisation
si il y a lieu(si iscotisation) """
# TODO : translate docstring to English
# TODO : change vente to purchase
2016-07-25 22:13:38 +00:00
vente = kwargs['instance']
if hasattr(vente, 'cotisation'):
vente.cotisation.vente = vente
vente.cotisation.save()
if vente.type_cotisation:
vente.create_cotis()
vente.cotisation.save()
2016-07-25 22:13:38 +00:00
user = vente.facture.user
2016-11-20 15:53:59 +00:00
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
2016-07-25 22:13:38 +00:00
2017-10-13 02:07:56 +00:00
# TODO : change vente to purchase
2016-07-25 22:13:38 +00:00
@receiver(post_delete, sender=Vente)
def vente_post_delete(sender, **kwargs):
2017-10-13 21:15:07 +00:00
"""Après suppression d'une vente, on synchronise l'user ldap (ex
suppression d'une cotisation"""
# TODO : translate docstring to English
# TODO : change vente to purchase
2016-07-25 22:13:38 +00:00
vente = kwargs['instance']
if vente.type_cotisation:
2016-07-25 22:13:38 +00:00
user = vente.facture.user
2016-11-20 15:53:59 +00:00
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
2016-07-25 22:13:38 +00:00
2017-10-13 02:07:56 +00:00
class Article(RevMixin, AclMixin, models.Model):
2017-10-13 02:07:56 +00:00
"""Liste des articles en vente : prix, nom, et attribut iscotisation
et duree si c'est une cotisation"""
# TODO : translate docstring to English
# TODO : Either use TYPE or TYPES in both choices but not both
USER_TYPES = (
('Adherent', _("Member")),
('Club', _("Club")),
('All', _("Both of them")),
)
COTISATION_TYPE = (
('Connexion', _("Connexion")),
('Adhesion', _("Membership")),
('All', _("Both of them")),
)
name = models.CharField(
max_length=255,
verbose_name=_("Designation")
)
# TODO : change prix to price
prix = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name=_("Unitary price")
)
duration = models.PositiveIntegerField(
2017-07-18 21:57:57 +00:00
blank=True,
null=True,
validators=[MinValueValidator(0)],
verbose_name=_("Duration (in whole month)")
)
type_user = models.CharField(
choices=USER_TYPES,
default='All',
max_length=255,
verbose_name=_("Type of users concerned")
)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
default=None,
blank=True,
null=True,
max_length=255,
verbose_name=_("Type of cotisation")
)
2017-06-26 17:23:01 +00:00
2017-10-29 10:57:18 +00:00
unique_together = ('name', 'type_user')
class Meta:
permissions = (
('view_article', _("Can see an article's details")),
)
verbose_name = "Article"
verbose_name_plural = "Articles"
2017-06-26 17:23:01 +00:00
def clean(self):
if self.name.lower() == 'solde':
raise ValidationError(
_("Solde is a reserved article name")
)
if self.type_cotisation and not self.duration:
raise ValidationError(
_("Duration must be specified for a cotisation")
)
2017-06-26 17:23:01 +00:00
2016-07-02 13:58:50 +00:00
def __str__(self):
return self.name
2017-10-13 02:07:56 +00:00
class Banque(RevMixin, AclMixin, models.Model):
2017-10-13 02:07:56 +00:00
"""Liste des banques"""
# TODO : translate docstring to English
name = models.CharField(
max_length=255,
verbose_name=_("Name")
)
2016-07-02 13:58:50 +00:00
class Meta:
permissions = (
('view_banque', _("Can see a bank's details")),
)
verbose_name=_("Bank")
verbose_name_plural=_("Banks")
2016-07-02 13:58:50 +00:00
def __str__(self):
return self.name
2017-10-13 02:07:56 +00:00
# TODO : change Paiement to Payment
class Paiement(RevMixin, AclMixin, models.Model):
2017-10-13 02:07:56 +00:00
"""Moyens de paiement"""
# TODO : translate docstring to English
PAYMENT_TYPES = (
(0, _("Standard")),
(1, _("Cheque")),
)
# TODO : change moyen to method
moyen = models.CharField(
max_length=255,
verbose_name=_("Method")
)
type_paiement = models.IntegerField(
choices=PAYMENT_TYPES,
default=0,
verbose_name=_("Payment type")
)
2016-07-02 13:58:50 +00:00
class Meta:
permissions = (
('view_paiement', _("Can see a payement's details")),
)
verbose_name = _("Payment method")
verbose_name_plural = _("Payment methods")
2016-07-02 13:58:50 +00:00
def __str__(self):
return self.moyen
2017-06-26 17:23:01 +00:00
def clean(self):
self.moyen = self.moyen.title()
2017-09-04 13:31:48 +00:00
def save(self, *args, **kwargs):
2017-10-13 02:07:56 +00:00
"""Un seul type de paiement peut-etre cheque..."""
# TODO : translate docstring to English
2017-09-04 13:31:48 +00:00
if Paiement.objects.filter(type_paiement=1).count() > 1:
raise ValidationError(
_("You cannot have multiple payment method of type cheque")
)
2017-09-04 13:31:48 +00:00
super(Paiement, self).save(*args, **kwargs)
2017-10-13 02:07:56 +00:00
class Cotisation(RevMixin, AclMixin, models.Model):
2017-10-13 02:07:56 +00:00
"""Objet cotisation, debut et fin, relié en onetoone à une vente"""
# TODO : translate docstring to English
COTISATION_TYPE = (
('Connexion', _("Connexion")),
('Adhesion', _("Membership")),
('All', _("Both of them")),
)
# TODO : change vente to purchase
vente = models.OneToOneField(
'Vente',
on_delete=models.CASCADE,
null=True,
verbose_name=_("Purchase")
)
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
max_length=255,
default='All',
verbose_name=_("Type of cotisation")
)
date_start = models.DateTimeField(
verbose_name=_("Starting date")
)
date_end = models.DateTimeField(
verbose_name=_("Ending date")
)
class Meta:
permissions = (
('view_cotisation', _("Can see a cotisation's details")),
('change_all_cotisation', _("Can edit the previous cotisations")),
)
def can_edit(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.change_cotisation'):
return False, _("You don't have the right to edit a cotisation.")
2017-12-31 19:13:41 +00:00
elif not user_request.has_perm('cotisations.change_all_cotisation') and\
2017-12-27 20:09:00 +00:00
(self.vente.facture.control or not self.vente.facture.valid):
return False, _("You don't have the right to edit a cotisation already controlled or invalidated.")
2017-12-27 20:09:00 +00:00
else:
return True, None
def can_delete(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.delete_cotisation'):
return False, _("You don't have the right to delete a cotisation.")
2017-12-27 20:09:00 +00:00
if self.vente.facture.control or not self.vente.facture.valid:
return False, _("You don't have the right to delete a cotisation already controlled or invalidated.")
2017-12-27 20:09:00 +00:00
else:
return True, None
def can_view(self, user_request, *args, **kwargs):
2017-12-31 19:13:41 +00:00
if not user_request.has_perm('cotisations.view_cotisation') and\
2017-12-27 20:09:00 +00:00
self.vente.facture.user != user_request:
return False, _("You don't have the right to see someone else's cotisation history.")
2017-12-27 20:09:00 +00:00
else:
return True, None
def __str__(self):
return str(self.vente)
2017-10-13 02:07:56 +00:00
@receiver(post_save, sender=Cotisation)
def cotisation_post_save(sender, **kwargs):
2017-10-13 21:15:07 +00:00
"""Après modification d'une cotisation, regeneration des services"""
# TODO : translate docstring to English
regen('dns')
regen('dhcp')
regen('mac_ip_list')
2017-09-14 18:03:28 +00:00
regen('mailing')
2017-10-13 02:07:56 +00:00
# TODO : should be name cotisation_post_delete
@receiver(post_delete, sender=Cotisation)
def vente_post_delete(sender, **kwargs):
2017-10-13 21:15:07 +00:00
"""Après suppression d'une vente, régénération des services"""
# TODO : translate docstring to English
cotisation = kwargs['instance']
2017-09-01 20:21:51 +00:00
regen('mac_ip_list')
2017-09-14 18:03:28 +00:00
regen('mailing')