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 && \ ...@@ -9,6 +9,11 @@ RUN apt update && \
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
rm -rf /var/lib/apt/lists/* 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/ COPY . /code/
# Comment what is not needed # Comment what is not needed
......
...@@ -6,13 +6,17 @@ ...@@ -6,13 +6,17 @@
## Installation sur un serveur ## 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 1. Paquets nécessaires
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi $ sudo apt install nginx python3 python3-pip python3-dev uwsgi
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl $ 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 2. Clonage du dépot
on se met au bon endroit : on se met au bon endroit :
......
...@@ -12,6 +12,7 @@ from activity.api.urls import register_activity_urls ...@@ -12,6 +12,7 @@ from activity.api.urls import register_activity_urls
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from member.api.urls import register_members_urls from member.api.urls import register_members_urls
from note.api.urls import register_note_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 logs.api.urls import register_logs_urls
from permission.api.urls import register_permission_urls from permission.api.urls import register_permission_urls
...@@ -74,6 +75,7 @@ router.register('user', UserViewSet) ...@@ -74,6 +75,7 @@ router.register('user', UserViewSet)
register_members_urls(router, 'members') register_members_urls(router, 'members')
register_activity_urls(router, 'activity') register_activity_urls(router, 'activity')
register_note_urls(router, 'note') register_note_urls(router, 'note')
register_treasury_urls(router, 'treasury')
register_permission_urls(router, 'permission') register_permission_urls(router, 'permission')
register_logs_urls(router, 'logs') register_logs_urls(router, 'logs')
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import datetime import datetime
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
...@@ -67,6 +68,13 @@ class Club(models.Model): ...@@ -67,6 +68,13 @@ class Club(models.Model):
email = models.EmailField( email = models.EmailField(
verbose_name=_('email'), verbose_name=_('email'),
) )
parent_club = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.PROTECT,
verbose_name=_('parent club'),
)
# Memberships # Memberships
membership_fee = models.PositiveIntegerField( membership_fee = models.PositiveIntegerField(
...@@ -158,6 +166,12 @@ class Membership(models.Model): ...@@ -158,6 +166,12 @@ class Membership(models.Model):
else: else:
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() 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: class Meta:
verbose_name = _('membership') verbose_name = _('membership')
verbose_name_plural = _('memberships') verbose_name_plural = _('memberships')
......
...@@ -89,21 +89,6 @@ ...@@ -89,21 +89,6 @@
"created_at": "2020-02-20T20:16:14.753Z" "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", "model": "note.noteclub",
"pk": 5, "pk": 5,
...@@ -256,4 +241,4 @@ ...@@ -256,4 +241,4 @@
"name": "Alcool" "name": "Alcool"
} }
} }
] ]
\ No newline at end of file
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .transactions import MembershipTransaction, Transaction, \ from .transactions import MembershipTransaction, Transaction, \
TemplateCategory, TransactionTemplate, RecurrentTransaction TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
__all__ = [ __all__ = [
# Notes # Notes
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions # Transactions
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'RecurrentTransaction', 'RecurrentTransaction', 'SpecialTransaction',
] ]
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models from django.db import models
from django.db.models import F
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
...@@ -93,12 +94,26 @@ class Transaction(PolymorphicModel): ...@@ -93,12 +94,26 @@ class Transaction(PolymorphicModel):
related_name='+', related_name='+',
verbose_name=_('source'), 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( destination = models.ForeignKey(
Note, Note,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='+', related_name='+',
verbose_name=_('destination'), 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( created_at = models.DateTimeField(
verbose_name=_('created at'), verbose_name=_('created at'),
default=timezone.now, default=timezone.now,
...@@ -115,11 +130,19 @@ class Transaction(PolymorphicModel): ...@@ -115,11 +130,19 @@ class Transaction(PolymorphicModel):
verbose_name=_('reason'), verbose_name=_('reason'),
max_length=255, max_length=255,
) )
valid = models.BooleanField( valid = models.BooleanField(
verbose_name=_('valid'), verbose_name=_('valid'),
default=True, default=True,
) )
invalidity_reason = models.CharField(
verbose_name=_('invalidity reason'),
max_length=255,
default=None,
null=True,
)
class Meta: class Meta:
verbose_name = _("transaction") verbose_name = _("transaction")
verbose_name_plural = _("transactions") verbose_name_plural = _("transactions")
...@@ -134,6 +157,13 @@ class Transaction(PolymorphicModel): ...@@ -134,6 +157,13 @@ class Transaction(PolymorphicModel):
When saving, also transfer money between two notes 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: if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered # When source == destination, no money is transfered
super().save(*args, **kwargs) super().save(*args, **kwargs)
...@@ -152,6 +182,10 @@ class Transaction(PolymorphicModel): ...@@ -152,6 +182,10 @@ class Transaction(PolymorphicModel):
self.source.balance -= to_transfer self.source.balance -= to_transfer
self.destination.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 # We save first the transaction, in case of the user has no right to transfer money
super().save(*args, **kwargs) super().save(*args, **kwargs)
......
...@@ -5,6 +5,7 @@ import html ...@@ -5,6 +5,7 @@ import html
import django_tables2 as tables import django_tables2 as tables
from django.db.models import F from django.db.models import F
from django.utils.html import format_html
from django_tables2.utils import A from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
...@@ -20,19 +21,48 @@ class HistoryTable(tables.Table): ...@@ -20,19 +21,48 @@ class HistoryTable(tables.Table):
'table table-condensed table-striped table-hover' 'table table-condensed table-striped table-hover'
} }
model = Transaction model = Transaction
exclude = ("id", "polymorphic_ctype", ) exclude = ("id", "polymorphic_ctype", "invalidity_reason", "source_alias", "destination_alias",)
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
sequence = ('...', 'type', 'total', 'valid', ) sequence = ('...', 'type', 'total', 'valid',)
orderable = False 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() type = tables.Column()
total = tables.Column() # will use Transaction.total() !! total = tables.Column() # will use Transaction.total() !!
valid = tables.Column(attrs={"td": {"id": lambda record: "validate_" + str(record.id), valid = tables.Column(
"class": lambda record: str(record.valid).lower() + ' validate', attrs={
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' "td": {
+ str(record.valid).lower() + ')'}}) "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): def order_total(self, queryset, is_descending):
# needed for rendering # needed for rendering
...@@ -53,8 +83,18 @@ class HistoryTable(tables.Table): ...@@ -53,8 +83,18 @@ class HistoryTable(tables.Table):
def render_reason(self, value): def render_reason(self, value):
return html.unescape(value) return html.unescape(value)
def render_valid(self, value): def render_valid(self, value, record):
return "" if value else "" """
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 # function delete_button(id) provided in template file
......
...@@ -18,5 +18,10 @@ def pretty_money(value): ...@@ -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 = template.Library()
register.filter('pretty_money', pretty_money) register.filter('pretty_money', pretty_money)
register.filter('cents_to_euros', cents_to_euros)
...@@ -28,4 +28,3 @@ class RolePermissionsAdmin(admin.ModelAdmin): ...@@ -28,4 +28,3 @@ class RolePermissionsAdmin(admin.ModelAdmin):
Admin customisation for RolePermissions Admin customisation for RolePermissions
""" """
list_display = ('role', ) list_display = ('role', )
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer from .serializers import PermissionSerializer
from ..models import Permission from ..models import Permission
......
...@@ -327,7 +327,7 @@ ...@@ -327,7 +327,7 @@
"note", "note",
"transaction" "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", "type": "add",
"mask": 1, "mask": 1,
"field": "", "field": "",
...@@ -387,7 +387,7 @@ ...@@ -387,7 +387,7 @@
"note", "note",
"recurrenttransaction" "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", "type": "add",
"mask": 2, "mask": 2,
"field": "", "field": "",
......
...@@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError ...@@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import F, Q, Model from django.db.models import F, Q, Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from member.models import Role from member.models import Role
...@@ -281,4 +280,3 @@ class RolePermissions(models.Model): ...@@ -281,4 +280,3 @@ class RolePermissions(models.Model):
def __str__(self): def __str__(self):
return str(self.role) return str(self.role)
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.permissions import DjangoObjectPermissions from rest_framework.permissions import DjangoObjectPermissions
from .backends import PermissionBackend from .backends import PermissionBackend
SAFE_METHODS = ('HEAD', 'OPTIONS', ) SAFE_METHODS = ('HEAD', 'OPTIONS', )
......
...@@ -3,10 +3,9 @@ ...@@ -3,10 +3,9 @@
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models.signals import pre_save, pre_delete, post_save, post_delete from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
from logs import signals as logs_signals from logs import signals as logs_signals
from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
EXCLUDED = [ EXCLUDED = [
......
...@@ -3,10 +3,8 @@ ...@@ -3,10 +3,8 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from django import template from django import template
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend 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)