Merge branch 'consos' into 'master'

Page de consommations

Closes #21

See merge request bde/nk20!57
This commit is contained in:
Pierre-antoine Comby 2020-03-21 23:18:24 +01:00
commit 0ecef74a1c
19 changed files with 1416 additions and 333 deletions

View File

@ -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")

View File

@ -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,
}

View File

@ -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):
"""

View File

@ -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,
},
),
}

View File

@ -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')

View File

@ -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:

View File

@ -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,

View File

@ -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

@ -1 +0,0 @@
Subproject commit 123466cfa914422422cd372197e64adf65ef05f7

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-11 11:44+0100\n"
"POT-Creation-Date: 2020-03-16 11:53+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -23,9 +23,10 @@ msgid "activity"
msgstr ""
#: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:60 apps/member/models.py:111
#: apps/member/models.py:61 apps/member/models.py:112
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202
#: templates/member/profile_detail.html:15
msgid "name"
msgstr ""
@ -50,7 +51,7 @@ msgid "description"
msgstr ""
#: apps/activity/models.py:54 apps/note/models/notes.py:164
#: apps/note/models/transactions.py:62
#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115
msgid "type"
msgstr ""
@ -86,7 +87,7 @@ msgstr ""
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
#: apps/logs/apps.py:11
msgid "Logs"
msgstr ""
@ -166,73 +167,73 @@ msgstr ""
msgid "user profile"
msgstr ""
#: apps/member/models.py:65
#: apps/member/models.py:66
msgid "email"
msgstr ""
#: apps/member/models.py:70
#: apps/member/models.py:71
msgid "membership fee"
msgstr ""
#: apps/member/models.py:74
#: apps/member/models.py:75
msgid "membership duration"
msgstr ""
#: apps/member/models.py:75
#: apps/member/models.py:76
msgid "The longest time a membership can last (NULL = infinite)."
msgstr ""
#: apps/member/models.py:80
#: apps/member/models.py:81
msgid "membership start"
msgstr ""
#: apps/member/models.py:81
#: apps/member/models.py:82
msgid "How long after January 1st the members can renew their membership."
msgstr ""
#: apps/member/models.py:86
#: apps/member/models.py:87
msgid "membership end"
msgstr ""
#: apps/member/models.py:87
#: apps/member/models.py:88
msgid ""
"How long the membership can last after January 1st of the next year after "
"members can renew their membership."
msgstr ""
#: apps/member/models.py:93 apps/note/models/notes.py:139
#: apps/member/models.py:94 apps/note/models/notes.py:139
msgid "club"
msgstr ""
#: apps/member/models.py:94
#: apps/member/models.py:95
msgid "clubs"
msgstr ""
#: apps/member/models.py:117
#: apps/member/models.py:118
msgid "role"
msgstr ""
#: apps/member/models.py:118
#: apps/member/models.py:119
msgid "roles"
msgstr ""
#: apps/member/models.py:142
#: apps/member/models.py:143
msgid "membership starts on"
msgstr ""
#: apps/member/models.py:145
#: apps/member/models.py:146
msgid "membership ends on"
msgstr ""
#: apps/member/models.py:149
#: apps/member/models.py:150
msgid "fee"
msgstr ""
#: apps/member/models.py:153
#: apps/member/models.py:154
msgid "membership"
msgstr ""
#: apps/member/models.py:154
#: apps/member/models.py:155
msgid "memberships"
msgstr ""
@ -253,12 +254,12 @@ msgstr ""
msgid "Alias successfully deleted"
msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source"
msgstr ""
#: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100
msgid "destination"
msgstr ""
@ -278,10 +279,6 @@ msgstr ""
msgid "Maximal size: 2MB"
msgstr ""
#: apps/note/forms.py:70
msgid "Source and destination must be different."
msgstr ""
#: apps/note/models/notes.py:27
msgid "account balance"
msgstr ""
@ -312,7 +309,7 @@ msgstr ""
msgid "display image"
msgstr ""
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
msgid "created at"
msgstr ""
@ -374,15 +371,15 @@ msgstr ""
msgid "aliases"
msgstr ""
#: apps/note/models/notes.py:229
#: apps/note/models/notes.py:233
msgid "Alias is too long."
msgstr ""
#: apps/note/models/notes.py:234
#: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists: {} "
msgstr ""
#: apps/note/models/notes.py:243
#: apps/note/models/notes.py:247
msgid "You can't delete your main alias."
msgstr ""
@ -398,7 +395,7 @@ msgstr ""
msgid "A template with this name already exist"
msgstr ""
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111
msgid "amount"
msgstr ""
@ -406,51 +403,81 @@ msgstr ""
msgid "in centimes"
msgstr ""
#: apps/note/models/transactions.py:74
#: apps/note/models/transactions.py:75
msgid "transaction template"
msgstr ""
#: apps/note/models/transactions.py:75
#: apps/note/models/transactions.py:76
msgid "transaction templates"
msgstr ""
#: apps/note/models/transactions.py:106
#: apps/note/models/transactions.py:107
msgid "quantity"
msgstr ""
#: apps/note/models/transactions.py:111
msgid "reason"
#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15
msgid "Gift"
msgstr ""
#: apps/note/models/transactions.py:115
msgid "valid"
#: apps/note/models/transactions.py:118 templates/base.html:90
#: templates/note/transaction_form.html:19
#: templates/note/transaction_form.html:126
msgid "Transfer"
msgstr ""
#: apps/note/models/transactions.py:120
msgid "transaction"
#: apps/note/models/transactions.py:119
msgid "Template"
msgstr ""
#: apps/note/models/transactions.py:121
msgid "transactions"
#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23
msgid "Credit"
msgstr ""
#: apps/note/models/transactions.py:185
#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27
msgid "Debit"
msgstr ""
#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230
msgid "membership transaction"
msgstr ""
#: apps/note/models/transactions.py:186
#: apps/note/models/transactions.py:129
msgid "reason"
msgstr ""
#: apps/note/models/transactions.py:133
msgid "valid"
msgstr ""
#: apps/note/models/transactions.py:138
msgid "transaction"
msgstr ""
#: apps/note/models/transactions.py:139
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:207
msgid "first_name"
msgstr ""
#: apps/note/models/transactions.py:212
msgid "bank"
msgstr ""
#: apps/note/models/transactions.py:231
msgid "membership transactions"
msgstr ""
#: apps/note/views.py:29
msgid "Transfer money from your account to one or others"
#: apps/note/views.py:31
msgid "Transfer money"
msgstr ""
#: apps/note/views.py:139
msgid "Consommations"
#: apps/note/views.py:132 templates/base.html:78
msgid "Consumptions"
msgstr ""
#: note_kfet/settings/__init__.py:63
#: note_kfet/settings/__init__.py:61
msgid ""
"The Central Authentication Service grants you access to most of our websites "
"by authenticating only once, so you don't need to type your credentials "
@ -473,24 +500,16 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note."
msgstr ""
#: templates/base.html:70
msgid "Consumptions"
msgstr ""
#: templates/base.html:73
#: templates/base.html:81
msgid "Clubs"
msgstr ""
#: templates/base.html:76
#: templates/base.html:84
msgid "Activities"
msgstr ""
#: templates/base.html:79
msgid "Button"
msgstr ""
#: templates/base.html:82 templates/note/transaction_form.html:35
msgid "Transfer"
#: templates/base.html:87
msgid "Buttons"
msgstr ""
#: templates/cas_server/base.html:7
@ -549,6 +568,7 @@ msgid "Field filters"
msgstr ""
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit"
msgstr ""
@ -572,6 +592,14 @@ msgstr ""
msgid "Transaction history"
msgstr ""
#: templates/member/club_form.html:6
msgid "Clubs list"
msgstr ""
#: templates/member/club_list.html:8
msgid "New club"
msgstr ""
#: templates/member/manage_auth_tokens.html:16
msgid "Token"
msgstr ""
@ -620,8 +648,87 @@ msgstr ""
msgid "Save Changes"
msgstr ""
#: templates/member/signup.html:5 templates/member/signup.html:8
#: templates/member/signup.html:14
msgid "Sign Up"
msgid "Sign up"
msgstr ""
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38
msgid "Select emitters"
msgstr ""
#: templates/note/conso_form.html:45
msgid "Select consumptions"
msgstr ""
#: templates/note/conso_form.html:51
msgid "Consume!"
msgstr ""
#: templates/note/conso_form.html:64
msgid "Most used buttons"
msgstr ""
#: templates/note/conso_form.html:121
msgid "Edit"
msgstr ""
#: templates/note/conso_form.html:126
msgid "Single consumptions"
msgstr ""
#: templates/note/conso_form.html:130
msgid "Double consumptions"
msgstr ""
#: templates/note/conso_form.html:141
msgid "Recent transactions history"
msgstr ""
#: templates/note/transaction_form.html:55
msgid "External payment"
msgstr ""
#: templates/note/transaction_form.html:63
msgid "Transfer type"
msgstr ""
#: templates/note/transaction_form.html:73
msgid "Name"
msgstr ""
#: templates/note/transaction_form.html:79
msgid "First name"
msgstr ""
#: templates/note/transaction_form.html:85
msgid "Bank"
msgstr ""
#: templates/note/transaction_form.html:97
#: templates/note/transaction_form.html:179
#: templates/note/transaction_form.html:186
msgid "Select receivers"
msgstr ""
#: templates/note/transaction_form.html:114
msgid "Amount"
msgstr ""
#: templates/note/transaction_form.html:119
msgid "Reason"
msgstr ""
#: templates/note/transaction_form.html:193
msgid "Credit note"
msgstr ""
#: templates/note/transaction_form.html:200
msgid "Debit note"
msgstr ""
#: templates/note/transactiontemplate_form.html:6
msgid "Buttons list"
msgstr ""
#: templates/registration/logged_out.html:8

View File

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-11 11:44+0100\n"
"POT-Creation-Date: 2020-03-16 11:53+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,9 +18,10 @@ msgid "activity"
msgstr "activité"
#: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:60 apps/member/models.py:111
#: apps/member/models.py:61 apps/member/models.py:112
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202
#: templates/member/profile_detail.html:15
msgid "name"
msgstr "nom"
@ -45,7 +46,7 @@ msgid "description"
msgstr "description"
#: apps/activity/models.py:54 apps/note/models/notes.py:164
#: apps/note/models/transactions.py:62
#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115
msgid "type"
msgstr "type"
@ -81,7 +82,7 @@ msgstr "invités"
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
#: apps/logs/apps.py:11
msgid "Logs"
msgstr ""
@ -161,37 +162,37 @@ msgstr "payé"
msgid "user profile"
msgstr "profil utilisateur"
#: apps/member/models.py:65
#: apps/member/models.py:66
msgid "email"
msgstr "courriel"
#: apps/member/models.py:70
#: apps/member/models.py:71
msgid "membership fee"
msgstr "cotisation pour adhérer"
#: apps/member/models.py:74
#: apps/member/models.py:75
msgid "membership duration"
msgstr "durée de l'adhésion"
#: apps/member/models.py:75
#: apps/member/models.py:76
msgid "The longest time a membership can last (NULL = infinite)."
msgstr "La durée maximale d'une adhésion (NULL = infinie)."
#: apps/member/models.py:80
#: apps/member/models.py:81
msgid "membership start"
msgstr "début de l'adhésion"
#: apps/member/models.py:81
#: apps/member/models.py:82
msgid "How long after January 1st the members can renew their membership."
msgstr ""
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
"adhésion."
#: apps/member/models.py:86
#: apps/member/models.py:87
msgid "membership end"
msgstr "fin de l'adhésion"
#: apps/member/models.py:87
#: apps/member/models.py:88
msgid ""
"How long the membership can last after January 1st of the next year after "
"members can renew their membership."
@ -199,45 +200,39 @@ msgstr ""
"Combien de temps l'adhésion peut durer après le 1er Janvier de l'année "
"suivante avant que les adhérents peuvent renouveler leur adhésion."
#: apps/member/models.py:93 apps/note/models/notes.py:139
#: apps/member/models.py:94 apps/note/models/notes.py:139
msgid "club"
msgstr "club"
msgid "New club"
msgstr "Nouveau club"
msgid "Clubs list"
msgstr "Liste des clubs"
#: apps/member/models.py:94
#: apps/member/models.py:95
msgid "clubs"
msgstr "clubs"
#: apps/member/models.py:117
#: apps/member/models.py:118
msgid "role"
msgstr "rôle"
#: apps/member/models.py:118
#: apps/member/models.py:119
msgid "roles"
msgstr "rôles"
#: apps/member/models.py:142
#: apps/member/models.py:143
msgid "membership starts on"
msgstr "l'adhésion commence le"
#: apps/member/models.py:145
#: apps/member/models.py:146
msgid "membership ends on"
msgstr "l'adhésion finie le"
#: apps/member/models.py:149
#: apps/member/models.py:150
msgid "fee"
msgstr "cotisation"
#: apps/member/models.py:153
#: apps/member/models.py:154
msgid "membership"
msgstr "adhésion"
#: apps/member/models.py:154
#: apps/member/models.py:155
msgid "memberships"
msgstr "adhésions"
@ -258,12 +253,12 @@ msgstr "Compte n°%(id)s : %(username)s"
msgid "Alias successfully deleted"
msgstr "L'alias a bien été supprimé"
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source"
msgstr "source"
#: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100
msgid "destination"
msgstr "destination"
@ -283,10 +278,6 @@ msgstr "Choisissez une image"
msgid "Maximal size: 2MB"
msgstr "Taille maximale : 2 Mo"
#: apps/note/forms.py:70
msgid "Source and destination must be different."
msgstr "La source et la destination doivent être différentes."
#: apps/note/models/notes.py:27
msgid "account balance"
msgstr "solde du compte"
@ -318,7 +309,7 @@ msgstr ""
msgid "display image"
msgstr "image affichée"
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
msgid "created at"
msgstr "créée le"
@ -380,15 +371,15 @@ msgstr "alias"
msgid "aliases"
msgstr "alias"
#: apps/note/models/notes.py:229
#: apps/note/models/notes.py:233
msgid "Alias is too long."
msgstr "L'alias est trop long."
#: apps/note/models/notes.py:234
#: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists: {} "
msgstr "Un alias avec un nom similaire existe déjà : {}"
#: apps/note/models/notes.py:243
#: apps/note/models/notes.py:247
msgid "You can't delete your main alias."
msgstr "Vous ne pouvez pas supprimer votre alias principal."
@ -404,7 +395,7 @@ msgstr "catégories de transaction"
msgid "A template with this name already exist"
msgstr "Un modèle de transaction avec un nom similaire existe déjà."
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111
msgid "amount"
msgstr "montant"
@ -412,47 +403,81 @@ msgstr "montant"
msgid "in centimes"
msgstr "en centimes"
#: apps/note/models/transactions.py:74
#: apps/note/models/transactions.py:75
msgid "transaction template"
msgstr "modèle de transaction"
#: apps/note/models/transactions.py:75
#: apps/note/models/transactions.py:76
msgid "transaction templates"
msgstr "modèles de transaction"
#: apps/note/models/transactions.py:106
#: apps/note/models/transactions.py:107
msgid "quantity"
msgstr "quantité"
#: apps/note/models/transactions.py:111
msgid "reason"
msgstr "raison"
#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15
msgid "Gift"
msgstr "Don"
#: apps/note/models/transactions.py:115
msgid "valid"
msgstr "valide"
#: apps/note/models/transactions.py:118 templates/base.html:90
#: templates/note/transaction_form.html:19
#: templates/note/transaction_form.html:126
msgid "Transfer"
msgstr "Virement"
#: apps/note/models/transactions.py:120
msgid "transaction"
msgstr "transaction"
#: apps/note/models/transactions.py:119
msgid "Template"
msgstr "Bouton"
#: apps/note/models/transactions.py:121
msgid "transactions"
msgstr "transactions"
#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23
msgid "Credit"
msgstr "Crédit"
#: apps/note/models/transactions.py:185
#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27
msgid "Debit"
msgstr "Retrait"
#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230
msgid "membership transaction"
msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:186
#: apps/note/models/transactions.py:129
msgid "reason"
msgstr "raison"
#: apps/note/models/transactions.py:133
msgid "valid"
msgstr "valide"
#: apps/note/models/transactions.py:138
msgid "transaction"
msgstr "transaction"
#: apps/note/models/transactions.py:139
msgid "transactions"
msgstr "transactions"
#: apps/note/models/transactions.py:207
msgid "first_name"
msgstr "Prénom"
#: apps/note/models/transactions.py:212
msgid "bank"
msgstr "Banque"
#: apps/note/models/transactions.py:231
msgid "membership transactions"
msgstr "transactions d'adhésion"
#: apps/note/views.py:29
msgid "Transfer money from your account to one or others"
msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres"
#: apps/note/views.py:31
msgid "Transfer money"
msgstr "Transferts d'argent"
#: note_kfet/settings/__init__.py:63
#: apps/note/views.py:132 templates/base.html:78
msgid "Consumptions"
msgstr "Consommations"
#: note_kfet/settings/__init__.py:61
msgid ""
"The Central Authentication Service grants you access to most of our websites "
"by authenticating only once, so you don't need to type your credentials "
@ -475,29 +500,18 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note."
msgstr "La note du BDE de l'ENS Paris-Saclay."
#: templates/base.html:70
msgid "Consumptions"
msgstr "Consommations"
#: templates/base.html:73
#: templates/base.html:81
msgid "Clubs"
msgstr "Clubs"
#: templates/base.html:76
#: templates/base.html:84
msgid "Activities"
msgstr "Activités"
#: templates/base.html:79
#: templates/base.html:87
msgid "Buttons"
msgstr "Boutons"
msgid "Buttons list"
msgstr "Liste des boutons"
#: templates/base.html:82 templates/note/transaction_form.html:35
msgid "Transfer"
msgstr "Virement"
#: templates/cas_server/base.html:7
msgid "Central Authentication Service"
msgstr ""
@ -556,8 +570,9 @@ msgid "Field filters"
msgstr ""
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit"
msgstr ""
msgstr "Envoyer"
#: templates/member/club_detail.html:10
msgid "Membership starts on"
@ -579,6 +594,14 @@ msgstr "solde du compte"
msgid "Transaction history"
msgstr "Historique des transactions"
#: templates/member/club_form.html:6
msgid "Clubs list"
msgstr "Liste des clubs"
#: templates/member/club_list.html:8
msgid "New club"
msgstr "Nouveau club"
#: templates/member/manage_auth_tokens.html:16
msgid "Token"
msgstr "Jeton"
@ -627,11 +650,89 @@ msgstr "Voir mes adhésions"
msgid "Save Changes"
msgstr "Sauvegarder les changements"
#: templates/member/signup.html:8
#: templates/member/signup.html:5 templates/member/signup.html:8
#: templates/member/signup.html:14
msgid "Sign up"
msgstr "Inscription"
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38
msgid "Select emitters"
msgstr "Sélection des émetteurs"
#: templates/note/conso_form.html:45
msgid "Select consumptions"
msgstr "Consommations"
#: templates/note/conso_form.html:51
msgid "Consume!"
msgstr "Consommer !"
#: templates/note/conso_form.html:64
msgid "Most used buttons"
msgstr "Boutons les plus utilisés"
#: templates/note/conso_form.html:121
msgid "Edit"
msgstr "Éditer"
#: templates/note/conso_form.html:126
msgid "Single consumptions"
msgstr "Consos simples"
#: templates/note/conso_form.html:130
msgid "Double consumptions"
msgstr "Consos doubles"
#: templates/note/conso_form.html:141
msgid "Recent transactions history"
msgstr "Historique des transactions récentes"
#: templates/note/transaction_form.html:55
msgid "External payment"
msgstr "Paiement extérieur"
#: templates/note/transaction_form.html:63
msgid "Transfer type"
msgstr "Type de transfert"
#: templates/note/transaction_form.html:73
msgid "Name"
msgstr "Nom"
#: templates/note/transaction_form.html:79
msgid "First name"
msgstr "Prénom"
#: templates/note/transaction_form.html:85
msgid "Bank"
msgstr "Banque"
#: templates/note/transaction_form.html:97
#: templates/note/transaction_form.html:179
#: templates/note/transaction_form.html:186
msgid "Select receivers"
msgstr "Sélection des destinataires"
#: templates/note/transaction_form.html:114
msgid "Amount"
msgstr "Montant"
#: templates/note/transaction_form.html:119
msgid "Reason"
msgstr "Raison"
#: templates/note/transaction_form.html:193
msgid "Credit note"
msgstr "Note à créditer"
#: templates/note/transaction_form.html:200
msgid "Debit note"
msgstr "Note à débiter"
#: templates/note/transactiontemplate_form.html:6
msgid "Buttons list"
msgstr "Liste des boutons"
#: templates/registration/logged_out.html:8
msgid "Thanks for spending some quality time with the Web site today."
msgstr ""

281
static/js/base.js Normal file
View File

@ -0,0 +1,281 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Convert balance in cents to a human readable amount
* @param value the balance, in cents
* @returns {string}
*/
function pretty_money(value) {
if (value % 100 === 0)
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €";
else
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "."
+ (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €";
}
/**
* Add a message on the top of the page.
* @param msg The message to display
* @param alert_type The type of the alert. Choices: info, success, warning, danger
*/
function addMsg(msg, alert_type) {
let msgDiv = $("#messages");
let html = msgDiv.html();
html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" +
"<button class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>"
+ msg + "</div>\n";
msgDiv.html(html);
}
/**
* Reload the balance of the user on the right top corner
*/
function refreshBalance() {
$("#user_balance").load("/ #user_balance");
}
/**
* Query the 20 first matched notes with a given pattern
* @param pattern The pattern that is queried
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/
function getMatchedNotes(pattern, fun) {
$.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun);
}
/**
* Generate a <li> entry with a given id and text
*/
function li(id, text) {
return "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" +
" id=\"" + id + "\">" + text + "</li>\n";
}
/**
* Render note name and picture
* @param note The note to render
* @param alias The alias to be displayed
* @param user_note_field
* @param profile_pic_field
*/
function displayNote(note, alias, user_note_field=null, profile_pic_field=null) {
let img = note == null ? null : note.display_image;
if (img == null)
img = '/media/pic/default.png';
if (note !== null && alias !== note.name)
alias += " (aka. " + note.name + ")";
if (note !== null && user_note_field !== null)
$("#" + user_note_field).text(alias + " : " + pretty_money(note.balance));
if (profile_pic_field != null)
$("#" + profile_pic_field).attr('src', img);
}
/**
* Remove a note from the emitters.
* @param d The note to remove
* @param note_prefix The prefix of the identifiers of the <li> blocks of the emitters
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @returns an anonymous function to be compatible with jQuery events
*/
function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) {
return (function() {
let new_notes_display = [];
let html = "";
notes_display.forEach(function (disp) {
if (disp.quantity > 1 || disp.id !== d.id) {
disp.quantity -= disp.id === d.id ? 1 : 0;
new_notes_display.push(disp);
html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>");
}
});
notes_display.length = 0;
new_notes_display.forEach(function(disp) {
notes_display.push(disp);
});
$("#" + note_list_id).html(html);
notes_display.forEach(function (disp) {
let obj = $("#" + note_prefix + "_" + disp.id);
obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
obj.hover(function() {
if (disp.note)
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
});
});
}
/**
* Generate an auto-complete field to query a note with its alias
* @param field_id The identifier of the text field where the alias is typed
* @param alias_matched_id The div block identifier where the matched aliases are displayed
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param notes An array containing the note objects of the buyers
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param alias_prefix The prefix of the <li> blocks for the matched aliases
* @param note_prefix The prefix of the <li> blocks for the notes of the buyers
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @param alias_click Function that is called when an alias is clicked. If this method exists and doesn't return true,
* the associated note is not displayed.
* Useful for a consumption if the item is selected before.
*/
function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes_display, alias_prefix="alias",
note_prefix="note", user_note_field=null, profile_pic_field=null, alias_click=null) {
let field = $("#" + field_id);
// When the user clicks on the search field, it is immediately cleared
field.click(function() {
field.val("");
});
let old_pattern = null;
// When the user type "Enter", the first alias is clicked
field.keypress(function(event) {
if (event.originalEvent.charCode === 13)
$("#" + alias_matched_id + " li").first().trigger("click");
});
// When the user type something, the matched aliases are refreshed
field.keyup(function(e) {
if (e.originalEvent.charCode === 13)
return;
let pattern = field.val();
// If the pattern is not modified, we don't query the API
if (pattern === old_pattern || pattern === "")
return;
old_pattern = pattern;
// Clear old matched notes
notes.length = 0;
let aliases_matched_obj = $("#" + alias_matched_id);
let aliases_matched_html = "";
// Get matched notes with the given pattern
getMatchedNotes(pattern, function(aliases) {
// The response arrived too late, we stop the request
if (pattern !== $("#" + field_id).val())
return;
aliases.results.forEach(function (alias) {
let note = alias.note;
aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name);
note.alias = alias;
notes.push(note);
});
// Display the list of matched aliases
aliases_matched_obj.html(aliases_matched_html);
notes.forEach(function (note) {
let alias = note.alias;
let alias_obj = $("#" + alias_prefix + "_" + alias.id);
// When an alias is hovered, the profile picture and the balance are displayed at the right place
alias_obj.hover(function () {
displayNote(note, alias.name, user_note_field, profile_pic_field);
});
// When the user click on an alias, the associated note is added to the emitters
alias_obj.click(function () {
field.val("");
// If the note is already an emitter, we increase the quantity
var disp = null;
notes_display.forEach(function (d) {
// We compare the note ids
if (d.id === note.id) {
d.quantity += 1;
disp = d;
}
});
// In the other case, we add a new emitter
if (disp == null) {
disp = {
name: alias.name,
id: note.id,
note: note,
quantity: 1
};
notes_display.push(disp);
}
// If the function alias_click exists, it is called. If it doesn't return true, then the notes are
// note displayed. Useful for a consumption when a button is already clicked
if (alias_click && !alias_click())
return;
let note_list = $("#" + note_list_id);
let html = "";
notes_display.forEach(function (disp) {
html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>");
});
// Emitters are displayed
note_list.html(html);
notes_display.forEach(function (disp) {
let line_obj = $("#" + note_prefix + "_" + disp.id);
// Hover an emitter display also the profile picture
line_obj.hover(function () {
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
// When an emitter is clicked, it is removed
line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field,
profile_pic_field));
});
});
});
});
});
}
// When a validate button is clicked, we switch the validation status
function de_validate(id, validated) {
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>");
// Perform a PATCH request to the API in order to update the transaction
// If the user has insuffisent rights, an error message will appear
$.ajax({
"url": "/api/note/transaction/transaction/" + id + "/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
"resourcetype": "TemplateTransaction",
valid: !validated
},
success: function () {
// Refresh jQuery objects
$(".validate").click(de_validate);
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
},
error: function(err) {
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + err.responseText, "danger");
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
}
});
}

205
static/js/consos.js Normal file
View File

@ -0,0 +1,205 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Refresh the history table on the consumptions page.
*/
function refreshHistory() {
$("#history").load("/note/consos/ #history");
$("#most_used").load("/note/consos/ #most_used");
}
$(document).ready(function() {
// If hash of a category in the URL, then select this category
// else select the first one
if (location.hash) {
$("a[href='" + location.hash + "']").tab("show");
} else {
$("a[data-toggle='tab']").first().tab("show");
}
// When selecting a category, change URL
$(document.body).on("click", "a[data-toggle='tab']", function() {
location.hash = this.getAttribute("href");
});
// Switching in double consumptions mode should update the layout
let double_conso_obj = $("#double_conso");
double_conso_obj.click(function() {
$("#consos_list_div").show();
$("#infos_div").attr('class', 'col-sm-5 col-xl-6');
$("#note_infos_div").attr('class', 'col-xl-3');
$("#user_select_div").attr('class', 'col-xl-4');
$("#buttons_div").attr('class', 'col-sm-7 col-xl-6');
let note_list_obj = $("#note_list");
if (buttons.length > 0 && note_list_obj.text().length > 0) {
$("#consos_list").html(note_list_obj.html());
note_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons,
"consos_list"));
});
}
});
let single_conso_obj = $("#single_conso");
single_conso_obj.click(function() {
$("#consos_list_div").hide();
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
$("#note_infos_div").attr('class', 'col-xl-5');
$("#user_select_div").attr('class', 'col-xl-7');
$("#buttons_div").attr('class', 'col-sm-7 col-md-8');
let consos_list_obj = $("#consos_list");
if (buttons.length > 0) {
if (notes_display.length === 0 && consos_list_obj.text().length > 0) {
$("#note_list").html(consos_list_obj.html());
consos_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons,
"note_list"));
});
}
else {
buttons.length = 0;
consos_list_obj.html("");
}
}
});
// Ensure we begin in single consumption. Removing these lines may cause problems when reloading.
single_conso_obj.prop('checked', 'true');
double_conso_obj.removeAttr('checked');
$("label[for='double_conso']").attr('class', 'btn btn-sm btn-outline-primary');
$("#consos_list_div").hide();
$("#consume_all").click(consumeAll);
});
notes = [];
notes_display = [];
buttons = [];
// When the user searches an alias, we update the auto-completion
autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display,
"alias", "note", "user_note", "profile_pic", function() {
if (buttons.length > 0 && $("#single_conso").is(":checked")) {
consumeAll();
return false;
}
return true;
});
/**
* Add a transaction from a button.
* @param dest Where the money goes
* @param amount The price of the item
* @param type The type of the transaction (content type id for TemplateTransaction)
* @param category_id The category identifier
* @param category_name The category name
* @param template_id The identifier of the button
* @param template_name The name of the button
*/
function addConso(dest, amount, type, category_id, category_name, template_id, template_name) {
var button = null;
buttons.forEach(function(b) {
if (b.id === template_id) {
b.quantity += 1;
button = b;
}
});
if (button == null) {
button = {
id: template_id,
name: template_name,
dest: dest,
quantity: 1,
amount: amount,
type: type,
category_id: category_id,
category_name: category_name
};
buttons.push(button);
}
let dc_obj = $("#double_conso");
if (dc_obj.is(":checked") || notes_display.length === 0) {
let list = dc_obj.is(":checked") ? "consos_list" : "note_list";
let html = "";
buttons.forEach(function(button) {
html += li("conso_button_" + button.id, button.name
+ "<span class=\"badge badge-dark badge-pill\">" + button.quantity + "</span>");
});
$("#" + list).html(html);
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, list));
});
}
else
consumeAll();
}
/**
* Reset the page as its initial state.
*/
function reset() {
notes_display.length = 0;
notes.length = 0;
buttons.length = 0;
$("#note_list").html("");
$("#alias_matched").html("");
$("#consos_list").html("");
displayNote(null, "");
refreshHistory();
refreshBalance();
}
/**
* Apply all transactions: all notes in `notes` buy each item in `buttons`
*/
function consumeAll() {
notes_display.forEach(function(note_display) {
buttons.forEach(function(button) {
consume(note_display.id, button.dest, button.quantity * note_display.quantity, button.amount,
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
});
});
}
/**
* Create a new transaction from a button through the API.
* @param source The note that paid the item (type: int)
* @param dest The note that sold the item (type: int)
* @param quantity The quantity sold (type: int)
* @param amount The price of one item, in cents (type: int)
* @param reason The transaction details (type: str)
* @param type The type of the transaction (content type id for TemplateTransaction)
* @param category The category id of the button (type: int)
* @param template The button id (type: int)
*/
function consume(source, dest, quantity, amount, reason, type, category, template) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": quantity,
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": type,
"resourcetype": "TemplateTransaction",
"source": source,
"destination": dest,
"category": category,
"template": template
}, reset).fail(function (e) {
reset();
addMsg("Une erreur est survenue lors de la transaction : " + e.responseText, "danger");
});
}

157
static/js/transfer.js Normal file
View File

@ -0,0 +1,157 @@
sources = [];
sources_notes_display = [];
dests = [];
dests_notes_display = [];
function refreshHistory() {
$("#history").load("/note/transfer/ #history");
}
function reset() {
sources_notes_display.length = 0;
sources.length = 0;
dests_notes_display.length = 0;
dests.length = 0;
$("#source_note_list").html("");
$("#dest_note_list").html("");
$("#source_alias_matched").html("");
$("#dest_alias_matched").html("");
$("#amount").val("");
$("#reason").val("");
$("#last_name").val("");
$("#first_name").val("");
$("#bank").val("");
refreshBalance();
refreshHistory();
}
$(document).ready(function() {
autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display,
"source_alias", "source_note", "user_note", "profile_pic");
autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display,
"dest_alias", "dest_note", "user_note", "profile_pic", function() {
let last = dests_notes_display[dests_notes_display.length - 1];
dests_notes_display.length = 0;
dests_notes_display.push(last);
last.quantity = 1;
$.getJSON("/api/user/" + last.note.user + "/", function(user) {
$("#last_name").val(user.last_name);
$("#first_name").val(user.first_name);
});
return true;
});
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading.
$("#type_gift").prop('checked', 'true');
$("#type_transfer").removeAttr('checked');
$("#type_credit").removeAttr('checked');
$("#type_debit").removeAttr('checked');
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
});
$("#transfer").click(function() {
if ($("#type_gift").is(':checked')) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": user_id,
"destination": dest.id
}, function () {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
reset();
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
reset();
});
});
}
else if ($("#type_transfer").is(':checked')) {
sources_notes_display.forEach(function (source) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": source.id,
"destination": dest.id
}, function () {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
reset();
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
reset();
});
});
});
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val();
let user_note = dests_notes_display[0].id;
let given_reason = $("#reason").val();
let source, dest, reason;
if ($("#type_credit").is(':checked')) {
source = special_note;
dest = user_note;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
else {
source = user_note;
dest = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1,
"amount": 100 * $("#amount").val(),
"reason": reason,
"valid": true,
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "SpecialTransaction",
"source": source,
"destination": dest,
"last_name": $("#last_name").val(),
"first_name": $("#first_name").val(),
"bank": $("#bank").val()
}, function () {
addMsg("Le crédit/retrait a bien été effectué !", "success");
reset();
}).fail(function (err) {
addMsg("Le crédit/transfert a échoué : " + err.responseText, "danger");
reset();
});
}
});

View File

@ -46,12 +46,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
crossorigin="anonymous"></script>
<script src="/static/js/base.js"></script>
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %}
{{ form.media }}
{% endif %}
<style>
.validate:hover {
cursor: pointer;
text-decoration: underline;
}
</style>
{% block extracss %}{% endblock %}
</head>
<body class="d-flex w-100 h-100 flex-column">
@ -86,7 +94,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %}
<li class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user"></i> {{ user.username }} ({{ user.note.balance | pretty_money }})
<i class="fa fa-user"></i>
<span id="user_balance">{{ user.username }} ({{ user.note.balance | pretty_money }})</span>
</a>
<div class="dropdown-menu dropdown-menu-right"
aria-labelledby="navbarDropdownMenuLink">
@ -115,6 +124,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</nav>
<div class="container-fluid my-3" style="max-width: 1600px;">
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
<div id="messages"></div>
{% block content %}
<p>Default content...</p>
{% endblock content %}
@ -158,6 +168,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
</footer>
<script>
CSRF_TOKEN = "{{ csrf_token }}";
</script>
{% block extrajavascript %}
{% endblock extrajavascript %}
</body>

View File

@ -10,7 +10,7 @@
<img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" >
</a>
</div>
<div class="card-body">
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
<dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd>
@ -76,11 +76,22 @@
</a>
</div>
<div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile">
<div id="history_list">
{% render_table history_list %}
</div>
</div>
</div>
</div>
{% endblock %}
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
}
</script>
{% endblock %}

View File

@ -7,67 +7,88 @@
{% block content %}
<div class="row mt-4">
<div class="col-sm-5 col-md-4">
<div class="col-sm-5 col-md-4" id="infos_div">
<div class="row">
{# User details column #}
<div class="col-xl-5">
<div class="col-xl-5" id="note_infos_div">
<div class="card border-success shadow mb-4">
<img src="https://perso.crans.org/erdnaxe/site-crans/img/logo.svg"
alt="" class="img-fluid rounded mx-auto d-block">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<div class="card-body text-center">
Paquito (aka. PAC) : -230 €
<span id="user_note"></span>
</div>
</div>
</div>
{# User selection column #}
<div class="col-xl-7">
<div class="col-xl-7" id="user_select_div">
<div class="card border-success shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
Sélection des émitteurs
{% trans "Select emitters" %}
</p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item py-1 d-flex justify-content-between align-items-center">
Cras justo odio
<span class="badge badge-dark badge-pill">14</span>
</li>
<li class="list-group-item py-1 d-flex justify-content-between align-items-center">
Dapibus ac facilisis in
<span class="badge badge-dark badge-pill">1</span>
</li>
<ul class="list-group list-group-flush" id="note_list">
</ul>
<div class="card-body">
TODO: reimplement select2 here in JS
<input class="form-control mx-auto d-block" type="text" id="note" />
<ul class="list-group list-group-flush" id="alias_matched">
</ul>
</div>
</div>
</div>
<div class="col-xl-5" id="consos_list_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Select consumptions" %}
</p>
</div>
<ul class="list-group list-group-flush" id="consos_list">
</ul>
<button id="consume_all" class="form-control btn btn-primary">
{% trans "Consume!" %}
</button>
</div>
</div>
</div>
</div>
{# Buttons column #}
<div class="col-sm-7 col-md-8">
<div class="col-sm-7 col-md-8" id="buttons_div">
{# Show last used buttons #}
<div class="card shadow mb-4">
<div class="card-body text-nowrap" style="overflow:auto hidden">
<p class="card-text text-muted font-weight-light font-italic">
Les boutons les plus utilisés s'afficheront ici.
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Most used buttons" %}
</p>
</div>
<div class="card-body text-nowrap" style="overflow:auto hidden">
<div class="d-inline-flex flex-wrap justify-content-center" id="most_used">
{% for button in most_used %}
{% if button.display %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
id="most_used_button{{ button.id }}" name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{# Regroup buttons under categories #}
{% regroup transaction_templates by template_type as template_types %}
{% regroup transaction_templates by category as categories %}
<div class="card border-primary text-center shadow mb-4">
{# Tabs for button categories #}
<div class="card-header">
<ul class="nav nav-tabs nav-fill card-header-tabs">
{% for template_type in template_types %}
{% for category in categories %}
<li class="nav-item">
<a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ template_type.grouper|slugify }}">
{{ template_type.grouper }}
<a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ category.grouper|slugify }}">
{{ category.grouper }}
</a>
</li>
{% endfor %}
@ -77,14 +98,16 @@
{# Tabs content #}
<div class="card-body">
<div class="tab-content">
{% for template_type in template_types %}
<div class="tab-pane" id="{{ template_type.grouper|slugify }}">
{% for category in categories %}
<div class="tab-pane" id="{{ category.grouper|slugify }}">
<div class="d-inline-flex flex-wrap justify-content-center">
{% for button in template_type.list %}
{% for button in category.list %}
{% if button.display %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
name="button" value="{{ button.name }}">
id="button{{ button.id }}" name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endif %}
{% endfor %}
</div>
</div>
@ -95,16 +118,16 @@
{# Mode switch #}
<div class="card-footer border-primary">
<a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}">
<i class="fa fa-edit"></i> Éditer
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
<div class="btn-group btn-group-toggle float-right" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary active">
<input type="radio" name="options" id="option1" checked>
Consomations simples
<label for="single_conso" class="btn btn-sm btn-outline-primary active">
<input type="radio" name="conso_type" id="single_conso" checked>
{% trans "Single consumptions" %}
</label>
<label class="btn btn-sm btn-outline-primary">
<input type="radio" name="options" id="option2">
Consomations doubles
<label for="double_conso" class="btn btn-sm btn-outline-primary">
<input type="radio" name="conso_type" id="double_conso">
{% trans "Double consumptions" %}
</label>
</div>
</div>
@ -112,40 +135,37 @@
</div>
</div>
<div class="card shadow mb-4">
<div class="card shadow mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">
Historique des transactions récentes
{% trans "Recent transactions history" %}
</p>
</div>
{% render_table table %}
</div>
{% endblock %}
{% block extracss %}
<style>
.select2-container{
max-width: 100%;
min-width: 100%;
}
</style>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript" src="/static/js/consos.js"></script>
<script type="text/javascript">
$(document).ready(function() {
// If hash of a category in the URL, then select this category
// else select the first one
if (location.hash) {
$("a[href='" + location.hash + "']").tab("show");
} else {
$("a[data-toggle='tab']").first().tab("show");
}
{% for button in most_used %}
{% if button.display %}
$("#most_used_button{{ button.id }}").click(function() {
addConso({{ button.destination.id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
{{ button.id }}, "{{ button.name }}");
});
{% endif %}
{% endfor %}
// When selecting a category, change URL
$(document.body).on("click", "a[data-toggle='tab']", function(event) {
location.hash = this.getAttribute("href");
});
{% for button in transaction_templates %}
{% if button.display %}
$("#button{{ button.id }}").click(function() {
addConso({{ button.destination.id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
{{ button.id }}, "{{ button.name }}");
});
{% endif %}
{% endfor %}
</script>
{% endblock %}

View File

@ -3,35 +3,188 @@
SPDX-License-Identifier: GPL-2.0-or-later
{% endcomment %}
{% load i18n static %}
{% load i18n static django_tables2 %}
{% block content %}
<form method="post" onsubmit="window.onbeforeunload=null">{% csrf_token %}
{% if form.non_field_errors %}
<p class="errornote">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
<label for="type_gift" class="btn btn-sm btn-outline-primary active">
<input type="radio" name="transaction_type" id="type_gift" checked>
{% trans "Gift" %}
</label>
<label for="type_transfer" class="btn btn-sm btn-outline-primary">
<input type="radio" name="transaction_type" id="type_transfer">
{% trans "Transfer" %}
</label>
<label for="type_credit" class="btn btn-sm btn-outline-primary">
<input type="radio" name="transaction_type" id="type_credit">
{% trans "Credit" %}
</label>
<label type="type_debit" class="btn btn-sm btn-outline-primary">
<input type="radio" name="transaction_type" id="type_debit">
{% trans "Debit" %}
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4" id="emitters_div" style="display: none;">
<div class="card border-success shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Select emitters" %}
</p>
{% endif %}
<fieldset class="module aligned">
{% for field in form %}
<div class="form-row{% if field.errors %} errors{% endif %}">
{{ field.errors }}
<div>
{{ field.label_tag }}
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field }}
{% endif %}
{% if field.field.help_text %}
<div class="help">{{ field.field.help_text|safe }}</div>
{% endif %}
</div>
<ul class="list-group list-group-flush" id="source_note_list">
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="source_note" />
<ul class="list-group list-group-flush" id="source_alias_matched">
</ul>
</div>
</div>
</div>
<div class="col-xl-4" id="note_infos_div">
<div class="card border-success shadow mb-4">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<div class="card-body text-center">
<span id="user_note"></span>
</div>
</div>
</div>
<div class="col-md-4" id="external_div" style="display: none;">
<div class="card border-success shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "External payment" %}
</p>
</div>
<ul class="list-group list-group-flush" id="source_note_list">
</ul>
<div class="card-body">
<div class="form-row">
<div class="col-md-12">
<label for="credit_type">{% trans "Transfer type" %} :</label>
<select id="credit_type" class="custom-select">
{% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %}
</fieldset>
<input type="submit" value="{% trans 'Transfer' %}">
</form>
</select>
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="last_name">{% trans "Name" %} :</label>
<input type="text" id="last_name" class="form-control" />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="first_name">{% trans "First name" %} :</label>
<input type="text" id="first_name" class="form-control" />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="bank">{% trans "Bank" %} :</label>
<input type="text" id="bank" class="form-control" />
</div>
</div>
</div>
</div>
</div>
<div class="col-md-8" id="dests_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold" id="dest_title">
{% trans "Select receivers" %}
</p>
</div>
<ul class="list-group list-group-flush" id="dest_note_list">
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="dest_note" />
<ul class="list-group list-group-flush" id="dest_alias_matched">
</ul>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="amount">{% trans "Amount" %} :</label>
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01" id="amount" />
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label for="reason">{% trans "Reason" %} :</label>
<input class="form-control mx-auto d-block" type="text" id="reason" required />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<button id="transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button>
</div>
</div>
<div class="card shadow mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Recent transactions history" %}
</p>
</div>
{% render_table table %}
</div>
{% endblock %}
{% block extrajavascript %}
<script>
TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }};
SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }};
user_id = {{ user.note.pk }};
$("#type_gift").click(function() {
$("#emitters_div").hide();
$("#external_div").hide();
$("#dests_div").attr('class', 'col-md-8');
$("#dest_title").text("{% trans "Select receivers" %}");
});
$("#type_transfer").click(function() {
$("#emitters_div").show();
$("#external_div").hide();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Select receivers" %}");
});
$("#type_credit").click(function() {
$("#emitters_div").hide();
$("#external_div").show();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Credit note" %}");
});
$("#type_debit").click(function() {
$("#emitters_div").hide();
$("#external_div").show();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Debit note" %}");
});
</script>
<script src="/static/js/transfer.js"></script>
{% endblock %}

View File

@ -15,7 +15,7 @@
<td><a href="{{object.get_absolute_url}}">{{ object.name }}</a></td>
<td>{{ object.destination }}</td>
<td>{{ object.amount | pretty_money }}</td>
<td>{{ object.template_type }}</td>
<td>{{ object.category }}</td>
</tr>
{% endfor %}
</table>