mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-08-03 22:24:34 +02:00
Merge branch 'consos' into 'master'
Page de consommations Closes #21 See merge request bde/nk20!57
This commit is contained in:
@@ -124,7 +124,7 @@ class UserDetailView(LoginRequiredMixin, DetailView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = context['user_object']
|
||||
history_list = \
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")
|
||||
context['history_list'] = HistoryTable(history_list)
|
||||
club_list = \
|
||||
Membership.objects.all().filter(user=user).only("club")
|
||||
|
@@ -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
|
||||
TemplateTransaction, SpecialTransaction
|
||||
|
||||
|
||||
class NoteSerializer(serializers.ModelSerializer):
|
||||
@@ -18,12 +18,6 @@ class NoteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {
|
||||
'view_name': 'project-detail',
|
||||
'lookup_field': 'pk'
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class NoteClubSerializer(serializers.ModelSerializer):
|
||||
@@ -31,44 +25,60 @@ 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__'
|
||||
|
||||
def get_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
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__'
|
||||
|
||||
def get_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
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__'
|
||||
|
||||
def get_name(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class AliasSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Aliases.
|
||||
The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API.
|
||||
"""
|
||||
note = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Alias
|
||||
fields = '__all__'
|
||||
|
||||
def get_note(self, alias):
|
||||
return NotePolymorphicSerializer().to_representation(alias.note)
|
||||
|
||||
|
||||
class NotePolymorphicSerializer(PolymorphicSerializer):
|
||||
model_serializer_mapping = {
|
||||
@@ -134,9 +144,21 @@ 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,
|
||||
MembershipTransaction: MembershipTransactionSerializer,
|
||||
SpecialTransaction: SpecialTransactionSerializer,
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
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 .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
|
||||
NoteUserSerializer, AliasSerializer, \
|
||||
@@ -61,6 +61,9 @@ 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):
|
||||
"""
|
||||
@@ -82,12 +85,11 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
|
||||
elif "club" in types:
|
||||
queryset = queryset.filter(polymorphic_ctype__model="noteclub")
|
||||
elif "special" in types:
|
||||
queryset = queryset.filter(
|
||||
polymorphic_ctype__model="notespecial")
|
||||
queryset = queryset.filter(polymorphic_ctype__model="notespecial")
|
||||
else:
|
||||
queryset = queryset.none()
|
||||
|
||||
return queryset
|
||||
return queryset.distinct()
|
||||
|
||||
|
||||
class AliasViewSet(viewsets.ModelViewSet):
|
||||
@@ -98,6 +100,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):
|
||||
"""
|
||||
|
@@ -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,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -156,11 +161,14 @@ class Transaction(PolymorphicModel):
|
||||
def total(self):
|
||||
return self.amount * self.quantity
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _('Transfer')
|
||||
|
||||
|
||||
class TemplateTransaction(Transaction):
|
||||
"""
|
||||
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
|
||||
|
||||
"""
|
||||
|
||||
template = models.ForeignKey(
|
||||
@@ -173,6 +181,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 +227,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,43 @@
|
||||
|
||||
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 .forms import TransactionForm, TransactionTemplateForm
|
||||
from .models import Transaction, TransactionTemplate, Alias
|
||||
from .forms import TransactionTemplateForm
|
||||
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, 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
|
||||
queryset = Transaction.objects.order_by("-id").all()[:50]
|
||||
template_name = "note/transaction_form.html"
|
||||
|
||||
# Transaction history table
|
||||
table_class = HistoryTable
|
||||
table_pagination = {"per_page": 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 +117,25 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
Consume
|
||||
"""
|
||||
model = Transaction
|
||||
queryset = Transaction.objects.order_by("-id").all()[:50]
|
||||
template_name = "note/conso_form.html"
|
||||
|
||||
# Transaction history table
|
||||
table_class = HistoryTable
|
||||
table_pagination = {"per_page": 10}
|
||||
table_pagination = {"per_page": 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(display=True) \
|
||||
.annotate(clicks=Count('templatetransaction')).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(TemplateTransaction).pk
|
||||
|
||||
# select2 compatibility
|
||||
context['no_cache'] = True
|
||||
|
Submodule apps/scripts deleted from 123466cfa9
Reference in New Issue
Block a user