Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • bde/nk20
  • mcngnt/nk20
2 results
Show changes
Commits on Source (54)
Showing
with 215 additions and 38 deletions
......@@ -9,6 +9,11 @@ RUN apt update && \
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
rm -rf /var/lib/apt/lists/*
# Install LaTeX requirements
RUN apt update && \
apt install -y texlive-latex-extra texlive-fonts-extra texlive-lang-french && \
rm -rf /var/lib/apt/lists/*
COPY . /code/
# Comment what is not needed
......
......@@ -6,13 +6,17 @@
## Installation sur un serveur
On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout nu ou bien configuré.
On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout nu ou bien configuré.
1. Paquets nécessaires
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl
La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante :
$ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french
2. Clonage du dépot
on se met au bon endroit :
......
......@@ -12,6 +12,7 @@ from activity.api.urls import register_activity_urls
from api.viewsets import ReadProtectedModelViewSet
from member.api.urls import register_members_urls
from note.api.urls import register_note_urls
from treasury.api.urls import register_treasury_urls
from logs.api.urls import register_logs_urls
from permission.api.urls import register_permission_urls
......@@ -74,6 +75,7 @@ router.register('user', UserViewSet)
register_members_urls(router, 'members')
register_activity_urls(router, 'activity')
register_note_urls(router, 'note')
register_treasury_urls(router, 'treasury')
register_permission_urls(router, 'permission')
register_logs_urls(router, 'logs')
......
......@@ -4,6 +4,7 @@
import datetime
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
......@@ -67,6 +68,13 @@ class Club(models.Model):
email = models.EmailField(
verbose_name=_('email'),
)
parent_club = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.PROTECT,
verbose_name=_('parent club'),
)
# Memberships
membership_fee = models.PositiveIntegerField(
......@@ -158,6 +166,12 @@ class Membership(models.Model):
else:
return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
def save(self, *args, **kwargs):
if self.club.parent_club is not None:
if not Membership.objects.filter(user=self.user, club=self.club.parent_club):
raise ValidationError(_('User is not a member of the parent club'))
super().save(*args, **kwargs)
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
......
......@@ -89,21 +89,6 @@
"created_at": "2020-02-20T20:16:14.753Z"
}
},
{
"model": "note.note",
"pk": 7,
"fields": {
"polymorphic_ctype": [
"note",
"noteuser"
],
"balance": 0,
"last_negative": null,
"is_active": true,
"display_image": "pic/default.png",
"created_at": "2020-03-22T13:01:35.680Z"
}
},
{
"model": "note.noteclub",
"pk": 5,
......@@ -256,4 +241,4 @@
"name": "Alcool"
}
}
]
]
\ No newline at end of file
......@@ -3,12 +3,12 @@
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .transactions import MembershipTransaction, Transaction, \
TemplateCategory, TransactionTemplate, RecurrentTransaction
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
__all__ = [
# Notes
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'RecurrentTransaction',
'RecurrentTransaction', 'SpecialTransaction',
]
......@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models
from django.db.models import F
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
......@@ -93,12 +94,26 @@ class Transaction(PolymorphicModel):
related_name='+',
verbose_name=_('source'),
)
source_alias = models.CharField(
max_length=255,
default="", # Will be remplaced by the name of the note on save
verbose_name=_('used alias'),
)
destination = models.ForeignKey(
Note,
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('destination'),
)
destination_alias = models.CharField(
max_length=255,
default="", # Will be remplaced by the name of the note on save
verbose_name=_('used alias'),
)
created_at = models.DateTimeField(
verbose_name=_('created at'),
default=timezone.now,
......@@ -115,11 +130,19 @@ class Transaction(PolymorphicModel):
verbose_name=_('reason'),
max_length=255,
)
valid = models.BooleanField(
verbose_name=_('valid'),
default=True,
)
invalidity_reason = models.CharField(
verbose_name=_('invalidity reason'),
max_length=255,
default=None,
null=True,
)
class Meta:
verbose_name = _("transaction")
verbose_name_plural = _("transactions")
......@@ -134,6 +157,13 @@ class Transaction(PolymorphicModel):
When saving, also transfer money between two notes
"""
# If the aliases are not entered, we assume that the used alias is the name of the note
if not self.source_alias:
self.source_alias = str(self.source)
if not self.destination_alias:
self.destination_alias = str(self.destination)
if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered
super().save(*args, **kwargs)
......@@ -152,6 +182,10 @@ class Transaction(PolymorphicModel):
self.source.balance -= to_transfer
self.destination.balance += to_transfer
# When a transaction is declared valid, we ensure that the invalidity reason is null, if it was
# previously invalid
self.invalidity_reason = None
# We save first the transaction, in case of the user has no right to transfer money
super().save(*args, **kwargs)
......
......@@ -5,6 +5,7 @@ import html
import django_tables2 as tables
from django.db.models import F
from django.utils.html import format_html
from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _
......@@ -20,19 +21,48 @@ class HistoryTable(tables.Table):
'table table-condensed table-striped table-hover'
}
model = Transaction
exclude = ("id", "polymorphic_ctype", )
exclude = ("id", "polymorphic_ctype", "invalidity_reason", "source_alias", "destination_alias",)
template_name = 'django_tables2/bootstrap4.html'
sequence = ('...', 'type', 'total', 'valid', )
sequence = ('...', 'type', 'total', 'valid',)
orderable = False
source = tables.Column(
attrs={
"td": {
"data-toggle": "tooltip",
"title": lambda record: _("used alias").capitalize() + " : " + record.source_alias,
}
}
)
destination = tables.Column(
attrs={
"td": {
"data-toggle": "tooltip",
"title": lambda record: _("used alias").capitalize() + " : " + record.destination_alias,
}
}
)
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() + ')'}})
valid = tables.Column(
attrs={
"td": {
"id": lambda record: "validate_" + str(record.id),
"class": lambda record: str(record.valid).lower() + ' validate',
"data-toggle": "tooltip",
"title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"),
"onclick": lambda record: 'in_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')',
"onmouseover": lambda record: '$("#invalidity_reason_'
+ str(record.id) + '").show();$("#invalidity_reason_'
+ str(record.id) + '").focus();',
"onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()',
}
}
)
def order_total(self, queryset, is_descending):
# needed for rendering
......@@ -53,8 +83,18 @@ class HistoryTable(tables.Table):
def render_reason(self, value):
return html.unescape(value)
def render_valid(self, value):
return "" if value else ""
def render_valid(self, value, record):
"""
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
"""
val = "" if value else ""
val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
+ "' value='" + (html.escape(record.invalidity_reason)
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
+ "'" + ("" if value else " disabled") \
+ " placeholder='" + html.escape(_("invalidity reason").capitalize()) + "'" \
+ " style='position: absolute; width: 15em; margin-left: -15.5em; margin-top: -2em; display: none;'>"
return format_html(val)
# function delete_button(id) provided in template file
......
......@@ -18,5 +18,10 @@ def pretty_money(value):
)
def cents_to_euros(value):
return "{:.02f}".format(value / 100) if value else ""
register = template.Library()
register.filter('pretty_money', pretty_money)
register.filter('cents_to_euros', cents_to_euros)
......@@ -28,4 +28,3 @@ class RolePermissionsAdmin(admin.ModelAdmin):
Admin customisation for RolePermissions
"""
list_display = ('role', )
......@@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer
from ..models import Permission
......
......@@ -327,7 +327,7 @@
"note",
"transaction"
],
"query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, {\"amount__lte\": [\"user\", \"note\", \"balance\"]}]",
"query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"amount__lte\": [\"user\", \"note\", \"balance\"]}, {\"valid\": false}]]",
"type": "add",
"mask": 1,
"field": "",
......@@ -387,7 +387,7 @@
"note",
"recurrenttransaction"
],
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]",
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 2,
"field": "",
......
......@@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Q, Model
from django.utils.translation import gettext_lazy as _
from member.models import Role
......@@ -281,4 +280,3 @@ class RolePermissions(models.Model):
def __str__(self):
return str(self.role)
......@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.permissions import DjangoObjectPermissions
from .backends import PermissionBackend
SAFE_METHODS = ('HEAD', 'OPTIONS', )
......
......@@ -3,10 +3,9 @@
from django.core.exceptions import PermissionDenied
from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
from logs import signals as logs_signals
from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
EXCLUDED = [
......
......@@ -3,10 +3,8 @@
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from django import template
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend
......
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'treasury.apps.TreasuryConfig'
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-lateré
from django.contrib import admin
from .models import RemittanceType, Remittance
@admin.register(RemittanceType)
class RemittanceTypeAdmin(admin.ModelAdmin):
"""
Admin customisation for RemiitanceType
"""
list_display = ('note', )
@admin.register(Remittance)
class RemittanceAdmin(admin.ModelAdmin):
"""
Admin customisation for Remittance
"""
list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', )
def has_change_permission(self, request, obj=None):
if not obj:
return True
return not obj.closed and super().has_change_permission(request, obj)
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from note.api.serializers import SpecialTransactionSerializer
from ..models import Invoice, Product, RemittanceType, Remittance
class ProductSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Product types.
The djangorestframework plugin will analyse the model `Product` and parse all fields in the API.
"""
class Meta:
model = Product
fields = '__all__'
class InvoiceSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Invoice types.
The djangorestframework plugin will analyse the model `Invoice` and parse all fields in the API.
"""
class Meta:
model = Invoice
fields = '__all__'
read_only_fields = ('bde',)
products = serializers.SerializerMethodField()
def get_products(self, obj):
return serializers.ListSerializer(child=ProductSerializer())\
.to_representation(Product.objects.filter(invoice=obj).all())
class RemittanceTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for RemittanceType types.
The djangorestframework plugin will analyse the model `RemittanceType` and parse all fields in the API.
"""
class Meta:
model = RemittanceType
fields = '__all__'
class RemittanceSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Remittance types.
The djangorestframework plugin will analyse the model `Remittance` and parse all fields in the API.
"""
transactions = serializers.SerializerMethodField()
class Meta:
model = Remittance
fields = '__all__'
def get_transactions(self, obj):
return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions)