mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 17:12:28 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master' into tresorerie
# Conflicts: # locale/de/LC_MESSAGES/django.po # locale/fr/LC_MESSAGES/django.po # note_kfet/settings/base.py # templates/base.html
This commit is contained in:
		@@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
 | 
			
		||||
 | 
			
		||||
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
 | 
			
		||||
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
 | 
			
		||||
    TemplateTransaction, MembershipTransaction
 | 
			
		||||
    RecurrentTransaction, MembershipTransaction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AliasInlines(admin.TabularInline):
 | 
			
		||||
@@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
 | 
			
		||||
    """
 | 
			
		||||
    Admin customisation for Transaction
 | 
			
		||||
    """
 | 
			
		||||
    child_models = (TemplateTransaction, MembershipTransaction)
 | 
			
		||||
    child_models = (RecurrentTransaction, MembershipTransaction)
 | 
			
		||||
    list_display = ('created_at', 'poly_source', 'poly_destination',
 | 
			
		||||
                    'quantity', 'amount', 'valid')
 | 
			
		||||
    list_filter = ('valid',)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ from rest_polymorphic.serializers import PolymorphicSerializer
 | 
			
		||||
 | 
			
		||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
 | 
			
		||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
 | 
			
		||||
    TemplateTransaction
 | 
			
		||||
    RecurrentTransaction, SpecialTransaction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -18,12 +18,7 @@ class NoteSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Note
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'url': {
 | 
			
		||||
                'view_name': 'project-detail',
 | 
			
		||||
                'lookup_field': 'pk'
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        read_only_fields = [f.name for f in model._meta.get_fields()]  # Notes are read-only protected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteClubSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -31,10 +26,15 @@ class NoteClubSerializer(serializers.ModelSerializer):
 | 
			
		||||
    REST API Serializer for Club's notes.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    name = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = NoteClub
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        read_only_fields = ('note', 'club', )
 | 
			
		||||
 | 
			
		||||
    def get_name(self, obj):
 | 
			
		||||
        return str(obj)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteSpecialSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -42,10 +42,15 @@ class NoteSpecialSerializer(serializers.ModelSerializer):
 | 
			
		||||
    REST API Serializer for special notes.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    name = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = NoteSpecial
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        read_only_fields = ('note', )
 | 
			
		||||
 | 
			
		||||
    def get_name(self, obj):
 | 
			
		||||
        return str(obj)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteUserSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -53,10 +58,15 @@ class NoteUserSerializer(serializers.ModelSerializer):
 | 
			
		||||
    REST API Serializer for User's notes.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    name = serializers.SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = NoteUser
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        read_only_fields = ('note', 'user', )
 | 
			
		||||
 | 
			
		||||
    def get_name(self, obj):
 | 
			
		||||
        return str(obj)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AliasSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -68,6 +78,7 @@ class AliasSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Alias
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        read_only_fields = ('note', )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotePolymorphicSerializer(PolymorphicSerializer):
 | 
			
		||||
@@ -78,6 +89,9 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
 | 
			
		||||
        NoteSpecial: NoteSpecialSerializer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Note
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemplateCategorySerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
@@ -112,14 +126,14 @@ class TransactionSerializer(serializers.ModelSerializer):
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemplateTransactionSerializer(serializers.ModelSerializer):
 | 
			
		||||
class RecurrentTransactionSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    REST API Serializer for Transactions.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `TemplateTransaction` and parse all fields in the API.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `RecurrentTransaction` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = TemplateTransaction
 | 
			
		||||
        model = RecurrentTransaction
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -134,9 +148,24 @@ class MembershipTransactionSerializer(serializers.ModelSerializer):
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecialTransactionSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    REST API Serializer for Special transactions.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `SpecialTransaction` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = SpecialTransaction
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionPolymorphicSerializer(PolymorphicSerializer):
 | 
			
		||||
    model_serializer_mapping = {
 | 
			
		||||
        Transaction: TransactionSerializer,
 | 
			
		||||
        TemplateTransaction: TemplateTransactionSerializer,
 | 
			
		||||
        RecurrentTransaction: RecurrentTransactionSerializer,
 | 
			
		||||
        MembershipTransaction: MembershipTransactionSerializer,
 | 
			
		||||
        SpecialTransaction: SpecialTransactionSerializer,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Transaction
 | 
			
		||||
 
 | 
			
		||||
@@ -3,57 +3,16 @@
 | 
			
		||||
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework.filters import SearchFilter
 | 
			
		||||
from rest_framework.filters import OrderingFilter, SearchFilter
 | 
			
		||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
 | 
			
		||||
 | 
			
		||||
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
 | 
			
		||||
    NoteUserSerializer, AliasSerializer, \
 | 
			
		||||
    TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
 | 
			
		||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
 | 
			
		||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \
 | 
			
		||||
    TransactionTemplateSerializer, TransactionPolymorphicSerializer
 | 
			
		||||
from ..models.notes import Note, Alias
 | 
			
		||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/note/note/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = Note.objects.all()
 | 
			
		||||
    serializer_class = NoteSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteClubViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/note/club/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = NoteClub.objects.all()
 | 
			
		||||
    serializer_class = NoteClubSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteSpecialViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/note/special/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = NoteSpecial.objects.all()
 | 
			
		||||
    serializer_class = NoteSpecialSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteUserViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/note/user/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = NoteUser.objects.all()
 | 
			
		||||
    serializer_class = NoteUserSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotePolymorphicViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -61,36 +20,27 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    queryset = Note.objects.all()
 | 
			
		||||
    serializer_class = NotePolymorphicSerializer
 | 
			
		||||
    filter_backends = [SearchFilter, OrderingFilter]
 | 
			
		||||
    search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
 | 
			
		||||
    ordering_fields = ['alias__name', 'alias__normalized_name']
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        """
 | 
			
		||||
        Parse query and apply filters.
 | 
			
		||||
        :return: The filtered set of requested notes
 | 
			
		||||
        """
 | 
			
		||||
        queryset = Note.objects.all()
 | 
			
		||||
        queryset = super().get_queryset()
 | 
			
		||||
 | 
			
		||||
        alias = self.request.query_params.get("alias", ".*")
 | 
			
		||||
        queryset = queryset.filter(
 | 
			
		||||
            Q(alias__name__regex="^" + alias)
 | 
			
		||||
            | Q(alias__normalized_name__regex="^" + Alias.normalize(alias))
 | 
			
		||||
            | Q(alias__normalized_name__regex="^" + alias.lower()))
 | 
			
		||||
 | 
			
		||||
        note_type = self.request.query_params.get("type", None)
 | 
			
		||||
        if note_type:
 | 
			
		||||
            types = str(note_type).lower()
 | 
			
		||||
            if "user" in types:
 | 
			
		||||
                queryset = queryset.filter(polymorphic_ctype__model="noteuser")
 | 
			
		||||
            elif "club" in types:
 | 
			
		||||
                queryset = queryset.filter(polymorphic_ctype__model="noteclub")
 | 
			
		||||
            elif "special" in types:
 | 
			
		||||
                queryset = queryset.filter(
 | 
			
		||||
                    polymorphic_ctype__model="notespecial")
 | 
			
		||||
            else:
 | 
			
		||||
                queryset = queryset.none()
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
        return queryset.distinct()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AliasViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class AliasViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -98,6 +48,9 @@ class AliasViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    queryset = Alias.objects.all()
 | 
			
		||||
    serializer_class = AliasSerializer
 | 
			
		||||
    filter_backends = [SearchFilter, OrderingFilter]
 | 
			
		||||
    search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
 | 
			
		||||
    ordering_fields = ['name', 'normalized_name']
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        """
 | 
			
		||||
@@ -105,35 +58,18 @@ class AliasViewSet(viewsets.ModelViewSet):
 | 
			
		||||
        :return: The filtered set of requested aliases
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        queryset = Alias.objects.all()
 | 
			
		||||
        queryset = super().get_queryset()
 | 
			
		||||
 | 
			
		||||
        alias = self.request.query_params.get("alias", ".*")
 | 
			
		||||
        queryset = queryset.filter(
 | 
			
		||||
            Q(name__regex="^" + alias) | Q(normalized_name__regex="^" + alias.lower()))
 | 
			
		||||
 | 
			
		||||
        note_id = self.request.query_params.get("note", None)
 | 
			
		||||
        if note_id:
 | 
			
		||||
            queryset = queryset.filter(id=note_id)
 | 
			
		||||
 | 
			
		||||
        note_type = self.request.query_params.get("type", None)
 | 
			
		||||
        if note_type:
 | 
			
		||||
            types = str(note_type).lower()
 | 
			
		||||
            if "user" in types:
 | 
			
		||||
                queryset = queryset.filter(
 | 
			
		||||
                    note__polymorphic_ctype__model="noteuser")
 | 
			
		||||
            elif "club" in types:
 | 
			
		||||
                queryset = queryset.filter(
 | 
			
		||||
                    note__polymorphic_ctype__model="noteclub")
 | 
			
		||||
            elif "special" in types:
 | 
			
		||||
                queryset = queryset.filter(
 | 
			
		||||
                    note__polymorphic_ctype__model="notespecial")
 | 
			
		||||
            else:
 | 
			
		||||
                queryset = queryset.none()
 | 
			
		||||
            Q(name__regex="^" + alias)
 | 
			
		||||
            | Q(normalized_name__regex="^" + Alias.normalize(alias))
 | 
			
		||||
            | Q(normalized_name__regex="^" + alias.lower()))
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemplateCategoryViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class TemplateCategoryViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -145,7 +81,7 @@ class TemplateCategoryViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    search_fields = ['$name', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class TransactionTemplateViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
@@ -157,7 +93,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    filterset_fields = ['name', 'amount', 'display', 'category', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionViewSet(viewsets.ModelViewSet):
 | 
			
		||||
class TransactionViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ from django import forms
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .models import Alias
 | 
			
		||||
from .models import Transaction, TransactionTemplate
 | 
			
		||||
from .models import TransactionTemplate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AliasForm(forms.ModelForm):
 | 
			
		||||
@@ -50,52 +50,3 @@ class TransactionTemplateForm(forms.ModelForm):
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionForm(forms.ModelForm):
 | 
			
		||||
    def save(self, commit=True):
 | 
			
		||||
        super().save(commit)
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        """
 | 
			
		||||
        If the user has no right to transfer funds, then it will be the source of the transfer by default.
 | 
			
		||||
        Transactions between a note and the same note are not authorized.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        cleaned_data = super().clean()
 | 
			
		||||
        if "source" not in cleaned_data:  # TODO Replace it with "if %user has no right to transfer funds"
 | 
			
		||||
            cleaned_data["source"] = self.user.note
 | 
			
		||||
 | 
			
		||||
        if cleaned_data["source"].pk == cleaned_data["destination"].pk:
 | 
			
		||||
            self.add_error("destination", _("Source and destination must be different."))
 | 
			
		||||
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Transaction
 | 
			
		||||
        fields = (
 | 
			
		||||
            'source',
 | 
			
		||||
            'destination',
 | 
			
		||||
            'reason',
 | 
			
		||||
            'amount',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Voir ci-dessus
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'source':
 | 
			
		||||
                autocomplete.ModelSelect2(
 | 
			
		||||
                    url='note:note_autocomplete',
 | 
			
		||||
                    attrs={
 | 
			
		||||
                        'data-placeholder': 'Note ...',
 | 
			
		||||
                        'data-minimum-input-length': 1,
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
            'destination':
 | 
			
		||||
                autocomplete.ModelSelect2(
 | 
			
		||||
                    url='note:note_autocomplete',
 | 
			
		||||
                    attrs={
 | 
			
		||||
                        'data-placeholder': 'Note ...',
 | 
			
		||||
                        'data-minimum-input-length': 1,
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@
 | 
			
		||||
 | 
			
		||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
 | 
			
		||||
from .transactions import MembershipTransaction, Transaction, \
 | 
			
		||||
    TemplateCategory, TransactionTemplate, TemplateTransaction
 | 
			
		||||
    TemplateCategory, TransactionTemplate, RecurrentTransaction
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    # Notes
 | 
			
		||||
    'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
 | 
			
		||||
    # Transactions
 | 
			
		||||
    'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
 | 
			
		||||
    'TemplateTransaction',
 | 
			
		||||
    'RecurrentTransaction',
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ from django.utils import timezone
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from polymorphic.models import PolymorphicModel
 | 
			
		||||
 | 
			
		||||
from .notes import Note, NoteClub
 | 
			
		||||
from .notes import Note, NoteClub, NoteSpecial
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Defines transactions
 | 
			
		||||
@@ -68,6 +68,7 @@ class TransactionTemplate(models.Model):
 | 
			
		||||
    description = models.CharField(
 | 
			
		||||
        verbose_name=_('description'),
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -106,7 +107,10 @@ class Transaction(PolymorphicModel):
 | 
			
		||||
        verbose_name=_('quantity'),
 | 
			
		||||
        default=1,
 | 
			
		||||
    )
 | 
			
		||||
    amount = models.PositiveIntegerField(verbose_name=_('amount'), )
 | 
			
		||||
    amount = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_('amount'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    reason = models.CharField(
 | 
			
		||||
        verbose_name=_('reason'),
 | 
			
		||||
        max_length=255,
 | 
			
		||||
@@ -132,6 +136,7 @@ class Transaction(PolymorphicModel):
 | 
			
		||||
 | 
			
		||||
        if self.source.pk == self.destination.pk:
 | 
			
		||||
            # When source == destination, no money is transfered
 | 
			
		||||
            super().save(*args, **kwargs)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        created = self.pk is None
 | 
			
		||||
@@ -147,20 +152,25 @@ class Transaction(PolymorphicModel):
 | 
			
		||||
            self.source.balance -= to_transfer
 | 
			
		||||
            self.destination.balance += to_transfer
 | 
			
		||||
 | 
			
		||||
        # We save first the transaction, in case of the user has no right to transfer money
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        # Save notes
 | 
			
		||||
        self.source.save()
 | 
			
		||||
        self.destination.save()
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def total(self):
 | 
			
		||||
        return self.amount * self.quantity
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def type(self):
 | 
			
		||||
        return _('Transfer')
 | 
			
		||||
 | 
			
		||||
class TemplateTransaction(Transaction):
 | 
			
		||||
 | 
			
		||||
class RecurrentTransaction(Transaction):
 | 
			
		||||
    """
 | 
			
		||||
    Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    template = models.ForeignKey(
 | 
			
		||||
@@ -173,6 +183,36 @@ class TemplateTransaction(Transaction):
 | 
			
		||||
        on_delete=models.PROTECT,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def type(self):
 | 
			
		||||
        return _('Template')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SpecialTransaction(Transaction):
 | 
			
		||||
    """
 | 
			
		||||
    Special type of :model:`note.Transaction` associated to transactions with special notes
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    last_name = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        verbose_name=_("name"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    first_name = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        verbose_name=_("first_name"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    bank = models.CharField(
 | 
			
		||||
        max_length=255,
 | 
			
		||||
        verbose_name=_("bank"),
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def type(self):
 | 
			
		||||
        return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MembershipTransaction(Transaction):
 | 
			
		||||
    """
 | 
			
		||||
@@ -189,3 +229,7 @@ class MembershipTransaction(Transaction):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("membership transaction")
 | 
			
		||||
        verbose_name_plural = _("membership transactions")
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def type(self):
 | 
			
		||||
        return _('membership transaction')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
import html
 | 
			
		||||
 | 
			
		||||
import django_tables2 as tables
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
from django_tables2.utils import A
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .models.notes import Alias
 | 
			
		||||
from .models.transactions import Transaction
 | 
			
		||||
@@ -17,17 +20,25 @@ class HistoryTable(tables.Table):
 | 
			
		||||
                'table table-condensed table-striped table-hover'
 | 
			
		||||
        }
 | 
			
		||||
        model = Transaction
 | 
			
		||||
        exclude = ("polymorphic_ctype", )
 | 
			
		||||
        exclude = ("id", "polymorphic_ctype", )
 | 
			
		||||
        template_name = 'django_tables2/bootstrap4.html'
 | 
			
		||||
        sequence = ('...', 'total', 'valid')
 | 
			
		||||
        sequence = ('...', 'type', 'total', 'valid', )
 | 
			
		||||
        orderable = False
 | 
			
		||||
 | 
			
		||||
    type = tables.Column()
 | 
			
		||||
 | 
			
		||||
    total = tables.Column()  # will use Transaction.total() !!
 | 
			
		||||
 | 
			
		||||
    valid = tables.Column(attrs={"td": {"id": lambda record: "validate_" + str(record.id),
 | 
			
		||||
                                        "class": lambda record: str(record.valid).lower() + ' validate',
 | 
			
		||||
                                        "onclick": lambda record: 'de_validate(' + str(record.id) + ', '
 | 
			
		||||
                                                                  + str(record.valid).lower() + ')'}})
 | 
			
		||||
 | 
			
		||||
    def order_total(self, queryset, is_descending):
 | 
			
		||||
        # needed for rendering
 | 
			
		||||
        queryset = queryset.annotate(total=F('amount') * F('quantity')) \
 | 
			
		||||
            .order_by(('-' if is_descending else '') + 'total')
 | 
			
		||||
        return (queryset, True)
 | 
			
		||||
        return queryset, True
 | 
			
		||||
 | 
			
		||||
    def render_amount(self, value):
 | 
			
		||||
        return pretty_money(value)
 | 
			
		||||
@@ -35,6 +46,16 @@ class HistoryTable(tables.Table):
 | 
			
		||||
    def render_total(self, value):
 | 
			
		||||
        return pretty_money(value)
 | 
			
		||||
 | 
			
		||||
    def render_type(self, value):
 | 
			
		||||
        return _(value)
 | 
			
		||||
 | 
			
		||||
    # Django-tables escape strings. That's a wrong thing.
 | 
			
		||||
    def render_reason(self, value):
 | 
			
		||||
        return html.unescape(value)
 | 
			
		||||
 | 
			
		||||
    def render_valid(self, value):
 | 
			
		||||
        return "✔" if value else "✖"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AliasTable(tables.Table):
 | 
			
		||||
    class Meta:
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ def pretty_money(value):
 | 
			
		||||
            abs(value) // 100,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        return "{:s}{:d} € {:02d}".format(
 | 
			
		||||
        return "{:s}{:d}.{:02d} €".format(
 | 
			
		||||
            "- " if value < 0 else "",
 | 
			
		||||
            abs(value) // 100,
 | 
			
		||||
            abs(value) % 100,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,53 +3,46 @@
 | 
			
		||||
 | 
			
		||||
from dal import autocomplete
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django.views.generic import CreateView, ListView, UpdateView
 | 
			
		||||
from django_tables2 import SingleTableView
 | 
			
		||||
from permission.backends import PermissionBackend
 | 
			
		||||
 | 
			
		||||
from .forms import TransactionForm, TransactionTemplateForm
 | 
			
		||||
from .models import Transaction, TransactionTemplate, Alias
 | 
			
		||||
from .forms import TransactionTemplateForm
 | 
			
		||||
from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial
 | 
			
		||||
from .models.transactions import SpecialTransaction
 | 
			
		||||
from .tables import HistoryTable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TransactionCreate(LoginRequiredMixin, CreateView):
 | 
			
		||||
class TransactionCreate(LoginRequiredMixin, SingleTableView):
 | 
			
		||||
    """
 | 
			
		||||
    Show transfer page
 | 
			
		||||
 | 
			
		||||
    TODO: If user have sufficient rights, they can transfer from an other note
 | 
			
		||||
    """
 | 
			
		||||
    model = Transaction
 | 
			
		||||
    form_class = TransactionForm
 | 
			
		||||
    template_name = "note/transaction_form.html"
 | 
			
		||||
 | 
			
		||||
    # Transaction history table
 | 
			
		||||
    table_class = HistoryTable
 | 
			
		||||
    table_pagination = {"per_page": 50}
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return Transaction.objects.filter(PermissionBackend.filter_queryset(
 | 
			
		||||
            self.request.user, Transaction, "view")
 | 
			
		||||
        ).order_by("-id").all()[:50]
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Add some context variables in template such as page title
 | 
			
		||||
        """
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        context['title'] = _('Transfer money from your account '
 | 
			
		||||
                             'to one or others')
 | 
			
		||||
 | 
			
		||||
        context['no_cache'] = True
 | 
			
		||||
        context['title'] = _('Transfer money')
 | 
			
		||||
        context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
 | 
			
		||||
        context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
 | 
			
		||||
        context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def get_form(self, form_class=None):
 | 
			
		||||
        """
 | 
			
		||||
        If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
 | 
			
		||||
        """
 | 
			
		||||
        form = super().get_form(form_class)
 | 
			
		||||
 | 
			
		||||
        if False:  # TODO: fix it with "if %user has no right to transfer funds"
 | 
			
		||||
            del form.fields['source']
 | 
			
		||||
            form.user = self.request.user
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse('note:transfer')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoteAutocomplete(autocomplete.Select2QuerySetView):
 | 
			
		||||
    """
 | 
			
		||||
@@ -127,21 +120,30 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
 | 
			
		||||
    """
 | 
			
		||||
    Consume
 | 
			
		||||
    """
 | 
			
		||||
    model = Transaction
 | 
			
		||||
    template_name = "note/conso_form.html"
 | 
			
		||||
 | 
			
		||||
    # Transaction history table
 | 
			
		||||
    table_class = HistoryTable
 | 
			
		||||
    table_pagination = {"per_page": 10}
 | 
			
		||||
    table_pagination = {"per_page": 50}
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return Transaction.objects.filter(
 | 
			
		||||
            PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
 | 
			
		||||
        ).order_by("-id").all()[:50]
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Add some context variables in template such as page title
 | 
			
		||||
        """
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
        context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
 | 
			
		||||
            .order_by('category')
 | 
			
		||||
        from django.db.models import Count
 | 
			
		||||
        buttons = TransactionTemplate.objects.filter(
 | 
			
		||||
            PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
 | 
			
		||||
        ).filter(display=True).annotate(clicks=Count('recurrenttransaction')).order_by('category__name', 'name')
 | 
			
		||||
        context['transaction_templates'] = buttons
 | 
			
		||||
        context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
 | 
			
		||||
        context['title'] = _("Consumptions")
 | 
			
		||||
        context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
 | 
			
		||||
 | 
			
		||||
        # select2 compatibility
 | 
			
		||||
        context['no_cache'] = True
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user