diff --git a/apps/logs/signals.py b/apps/logs/signals.py
index 43fc1e13a92d23e746fbb5cab92bbbdaa6d70cf4..37e9ba79dc3176db2b529b799adeb5b457791014 100644
--- a/apps/logs/signals.py
+++ b/apps/logs/signals.py
@@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_force_save"):
+        return
+
     # noinspection PyProtectedMember
     previous = instance._previous
 
@@ -106,6 +109,9 @@ def delete_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_force_delete"):
+        return
+
     # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
     user, ip = get_current_authenticated_user(), get_current_ip()
 
diff --git a/apps/member/fixtures/initial.json b/apps/member/fixtures/initial.json
index 094d2c3f9306f4eed438ec9607d9755151842874..d72377f8afb514df348bd0548ab7ad61bab3fcbc 100644
--- a/apps/member/fixtures/initial.json
+++ b/apps/member/fixtures/initial.json
@@ -18,6 +18,7 @@
         "fields": {
             "name": "Kfet",
             "email": "tresorerie.bde@example.com",
+            "parent_club": 1,
             "require_memberships":  true,
             "membership_fee": 3500,
             "membership_duration": 396,
diff --git a/apps/member/forms.py b/apps/member/forms.py
index 9ac4ffa99810f9e3548107dac363d0ac16766a7a..87d5322e793fffede7dee092b7ff8d7de93e92d2 100644
--- a/apps/member/forms.py
+++ b/apps/member/forms.py
@@ -4,6 +4,7 @@
 from django import forms
 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
 from django.contrib.auth.models import User
+from django.utils.translation import gettext_lazy as _
 from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
 from permission.models import PermissionMask
 
@@ -57,11 +58,6 @@ class ClubForm(forms.ModelForm):
         }
 
 
-class AddMembersForm(forms.Form):
-    class Meta:
-        fields = ('',)
-
-
 class MembershipForm(forms.ModelForm):
     class Meta:
         model = Membership
diff --git a/apps/member/models.py b/apps/member/models.py
index 8906af9cd790f6aca30d6a173447c992611d688e..180839d8d950d00162e8fd28d02a9c37527c928d 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -4,11 +4,14 @@
 import datetime
 
 from django.conf import settings
-from django.core.exceptions import ValidationError
+from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError, PermissionDenied
 from django.db import models
 from django.urls import reverse, reverse_lazy
 from django.utils.translation import gettext_lazy as _
 
+from note.models import MembershipTransaction
+
 
 class Profile(models.Model):
     """
@@ -91,7 +94,7 @@ class Club(models.Model):
         verbose_name=_('membership fee'),
     )
 
-    membership_duration = models.IntegerField(
+    membership_duration = models.PositiveIntegerField(
         blank=True,
         null=True,
         verbose_name=_('membership duration'),
@@ -174,7 +177,7 @@ class Membership(models.Model):
 
     """
     user = models.ForeignKey(
-        settings.AUTH_USER_MODEL,
+        User,
         on_delete=models.PROTECT,
     )
 
@@ -185,6 +188,7 @@ class Membership(models.Model):
 
     roles = models.ManyToManyField(
         Role,
+        verbose_name=_("roles"),
     )
 
     date_start = models.DateField(
@@ -209,17 +213,41 @@ class Membership(models.Model):
     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).exists():
-                raise ValidationError(_('User is not a member of the parent club'))
+                raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
 
         created = not self.pk
         if created:
+            if Membership.objects.filter(
+                    user=self.user,
+                    club=self.club,
+                    date_start__lte=datetime.datetime.now().date(),
+                    date_end__gte=datetime.datetime.now().date(),
+            ).exists():
+                raise ValidationError(_('User is already a member of the club'))
+
             self.fee = self.club.membership_fee
-            self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration)
-            if self.date_end > self.club.membership_end:
+            if self.club.membership_duration is not None:
+                self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration)
+            else:
+                self.date_end = self.date_start + datetime.timedelta(days=0x7FFFFFFF)
+            if self.club.membership_end is not None and self.date_end > self.club.membership_end:
                 self.date_end = self.club.membership_end
 
         super().save(*args, **kwargs)
 
+        if created and self.fee:
+            try:
+                MembershipTransaction.objects.create(
+                    membership=self,
+                    source=self.user.note,
+                    destination=self.club.note,
+                    quantity=1,
+                    amount=self.fee,
+                    reason="Adhésion",
+                )
+            except PermissionDenied:
+                self.delete()
+
     class Meta:
         verbose_name = _('membership')
         verbose_name_plural = _('memberships')
diff --git a/apps/member/tables.py b/apps/member/tables.py
index d0c37a6e2e47baaa3e4b2f3eef1eebe152fdc5d1..18a2a555701fd69a06ca68503cbdb3f571aa49fc 100644
--- a/apps/member/tables.py
+++ b/apps/member/tables.py
@@ -3,8 +3,14 @@
 
 import django_tables2 as tables
 from django.contrib.auth.models import User
+from django.urls import reverse_lazy
+from django.utils.html import format_html
+from django_tables2 import A
 
-from .models import Club
+from note.templatetags.pretty_money import pretty_money
+from note_kfet.middlewares import get_current_authenticated_user
+from permission.backends import PermissionBackend
+from .models import Club, Membership
 
 
 class ClubTable(tables.Table):
@@ -33,3 +39,33 @@ class UserTable(tables.Table):
         template_name = 'django_tables2/bootstrap4.html'
         fields = ('last_name', 'first_name', 'username', 'email')
         model = User
+
+
+class MembershipTable(tables.Table):
+    roles = tables.Column(
+        attrs={
+            "td": {
+                "class": "text-truncate",
+            }
+        }
+    )
+
+    def render_fee(self, value):
+        return pretty_money(value)
+
+    def render_roles(self, record):
+        roles = record.roles.all()
+        s = ", ".join(str(role) for role in roles)
+        if PermissionBackend().has_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
+            s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
+                            + "'>" + s + "</a>")
+        return s
+
+    class Meta:
+        attrs = {
+            'class': 'table table-condensed table-striped table-hover',
+            'style': 'table-layout: fixed;'
+        }
+        template_name = 'django_tables2/bootstrap4.html'
+        fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
+        model = Membership
diff --git a/apps/member/urls.py b/apps/member/urls.py
index 7f4bf0d51de37a5b62987c66fc56c0ab9c37f108..2000a6e4f47f531770d278087a2c53dc315d5ddd 100644
--- a/apps/member/urls.py
+++ b/apps/member/urls.py
@@ -12,15 +12,16 @@ urlpatterns = [
     path('club/', views.ClubListView.as_view(), name="club_list"),
     path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
     path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
+    path('club/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"),
     path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
     path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
     path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
     path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
 
     path('user/', views.UserListView.as_view(), name="user_list"),
-    path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
-    path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"),
-    path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
-    path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"),
+    path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),
+    path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
+    path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
+    path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
     path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
 ]
diff --git a/apps/member/views.py b/apps/member/views.py
index 651dfc35298d66519649ee550d70c4607f1f64a8..ad01e2a2bbdf2884a42839642acd5e1d5fff7895 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -10,6 +10,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import User
 from django.contrib.auth.views import LoginView
 from django.db.models import Q
+from django.forms import HiddenInput
 from django.shortcuts import redirect
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
@@ -27,7 +28,7 @@ from permission.views import ProtectQuerysetMixin
 from .filters import UserFilter, UserFilterFormHelper
 from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
 from .models import Club, Membership
-from .tables import ClubTable, UserTable
+from .tables import ClubTable, UserTable, MembershipTable
 
 
 class CustomLoginView(LoginView):
@@ -138,7 +139,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
         context['history_list'] = HistoryTable(history_list)
         club_list = Membership.objects.all().filter(user=user)\
             .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).only("club")
-        context['club_list'] = ClubTable(club_list)
+        context['club_list'] = MembershipTable(data=club_list)
         return context
 
 
@@ -294,7 +295,19 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
             date_start__lte=datetime.now().date(),
             date_end__gte=datetime.now().date(),
         ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).all()
-        context['member_list'] = club_member
+
+        context['member_list'] = MembershipTable(data=club_member)
+
+        empty_membership = Membership(
+            club=club,
+            user=User.objects.first(),
+            date_start=datetime.now().date(),
+            date_end=datetime.now().date(),
+            fee=0,
+        )
+        context["can_add_members"] = PermissionBackend()\
+            .has_perm(self.request.user, "member.add_membership", empty_membership)
+
         return context
 
 
@@ -339,7 +352,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
             .get(pk=self.kwargs["pk"])
         context = super().get_context_data(**kwargs)
         context['club'] = club
-        context['no_cache'] = True
 
         return context
 
@@ -347,6 +359,63 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
         club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
             .get(pk=self.kwargs["pk"])
         form.instance.club = club
+
+        if club.parent_club is not None:
+            if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists():
+                form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
+                return super().form_invalid(form)
+
+        if Membership.objects.filter(
+                user=form.instance.user,
+                club=club,
+                date_start__lte=datetime.now().date(),
+                date_end__gte=datetime.now().date(),
+        ).exists():
+            form.add_error('user', _('User is already a member of the club'))
+            return super().form_invalid(form)
+
+        if form.instance.date_start < form.instance.club.membership_start:
+            form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
+        if form.instance.date_start > form.instance.club.membership_end:
+            form.add_error('user', _("The membership must end before {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
+        return super().form_valid(form)
+
+    def get_success_url(self):
+        return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
+
+
+class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
+    model = Membership
+    form_class = MembershipForm
+    template_name = 'member/add_members.html'
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        club = self.object.club
+        context['club'] = club
+        form = context['form']
+        form.fields['user'].disabled = True
+        form.fields['date_start'].widget = HiddenInput()
+
+        return context
+
+    def form_valid(self, form):
+        if form.instance.date_start < form.instance.club.membership_start:
+            form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
+        if form.instance.date_start > form.instance.club.membership_end:
+            form.add_error('user', _("The membership must end before {:%m-%d-%Y}.")
+                           .format(form.instance.club.membership_start))
+            return super().form_invalid(form)
+
         return super().form_valid(form)
 
     def get_success_url(self):
diff --git a/apps/note/admin.py b/apps/note/admin.py
index 702d3350e7933ac7141b80bdd1ab884ce515e1b7..7b4ba870c64331eb10501ba13ef68df493418127 100644
--- a/apps/note/admin.py
+++ b/apps/note/admin.py
@@ -138,6 +138,13 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
         return []
 
 
+@admin.register(MembershipTransaction)
+class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
+    """
+    Admin customisation for Transaction
+    """
+
+
 @admin.register(TransactionTemplate)
 class TransactionTemplateAdmin(admin.ModelAdmin):
     """
diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py
index d1dcd7887eb62c36f7235e90d0b12454c80ce58e..d9b860ae823486c41c3c9e49d8da44ccfde12b53 100644
--- a/apps/note/models/transactions.py
+++ b/apps/note/models/transactions.py
@@ -140,6 +140,7 @@ class Transaction(PolymorphicModel):
         max_length=255,
         default=None,
         null=True,
+        blank=True,
     )
 
     class Meta:
diff --git a/apps/permission/backends.py b/apps/permission/backends.py
index f478cd5dcc4f5deaf298c369e25078c98838e08d..b95f8203ab7c3c14dadb45434800920491bc09df 100644
--- a/apps/permission/backends.py
+++ b/apps/permission/backends.py
@@ -1,6 +1,8 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
+import datetime
+
 from django.contrib.auth.backends import ModelBackend
 from django.contrib.auth.models import User, AnonymousUser
 from django.contrib.contenttypes.models import ContentType
@@ -32,7 +34,8 @@ class PermissionBackend(ModelBackend):
         for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
                 .filter(
             rolepermissions__role__membership__user=user,
-            rolepermissions__role__membership__valid=True,
+            rolepermissions__role__membership__date_start__lte=datetime.date.today(),
+            rolepermissions__role__membership__date_end__gte=datetime.date.today(),
             model__app_label=model.app_label,  # For polymorphic models, we don't filter on model type
             type=type,
         ).all():
diff --git a/apps/permission/models.py b/apps/permission/models.py
index d1b55090f04b82af44e7dc80258f50440b3694d0..c8df07c0cfa9f75539edc9414fdbde590380a06e 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -45,11 +45,13 @@ class InstancedPermission:
                 else:
                     oldpk = obj.pk
                 # Ensure previous models are deleted
-                self.model.model_class().objects.filter(pk=obj.pk).delete()
+                self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk") + 1).delete()
                 # Force insertion, no data verification, no trigger
+                obj._force_save = True
                 Model.save(obj, force_insert=True)
                 ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists()
                 # Delete testing object
+                obj._force_delete = True
                 Model.delete(obj)
 
                 # If the primary key was specified, we restore it
diff --git a/apps/permission/signals.py b/apps/permission/signals.py
index 1e30f56f5793296e929b195231bb4a4eb4660d26..5ccb9c0bbeb897f535bf3ba9d2b5a64beae8e0e6 100644
--- a/apps/permission/signals.py
+++ b/apps/permission/signals.py
@@ -29,6 +29,9 @@ def pre_save_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_force_save"):
+        return
+
     user = get_current_authenticated_user()
     if user is None:
         # Action performed on shell is always granted
@@ -58,32 +61,14 @@ def pre_save_object(sender, instance, **kwargs):
             if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
                 raise PermissionDenied
     else:
-        # We check if the user can add the model
-
-        # While checking permissions, the object will be inserted in the DB, then removed.
-        # We disable temporary the connectors
-        pre_save.disconnect(pre_save_object)
-        pre_delete.disconnect(pre_delete_object)
-        # We disable also logs connectors
-        pre_save.disconnect(logs_signals.pre_save_object)
-        post_save.disconnect(logs_signals.save_object)
-        post_delete.disconnect(logs_signals.delete_object)
-
         # We check if the user has right to add the object
         has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance)
 
-        # Then we reconnect all
-        pre_save.connect(pre_save_object)
-        pre_delete.connect(pre_delete_object)
-        pre_save.connect(logs_signals.pre_save_object)
-        post_save.connect(logs_signals.save_object)
-        post_delete.connect(logs_signals.delete_object)
-
         if not has_perm:
             raise PermissionDenied
 
 
-def pre_delete_object(sender, instance, **kwargs):
+def pre_delete_object(instance, **kwargs):
     """
     Before a model get deleted, we check the permissions
     """
@@ -91,6 +76,9 @@ def pre_delete_object(sender, instance, **kwargs):
     if instance._meta.label_lower in EXCLUDED:
         return
 
+    if hasattr(instance, "_force_delete"):
+        return
+
     user = get_current_authenticated_user()
     if user is None:
         # Action performed on shell is always granted
diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py
index 81f55deead00755acd5938547939afd3c8e3ed5a..ecd758e058cfa995f54974bec65e12503a80660b 100644
--- a/note_kfet/inputs.py
+++ b/note_kfet/inputs.py
@@ -1,7 +1,6 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-import datetime
 from json import dumps as json_dumps
 
 from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput
diff --git a/templates/member/club_info.html b/templates/member/club_info.html
index da747ccd88e4866b6941d837b39a69d7c2057ee5..23a41c1c3ab4c59b3656465ab17e5aa410976162 100644
--- a/templates/member/club_info.html
+++ b/templates/member/club_info.html
@@ -1,4 +1,4 @@
-{% load i18n static pretty_money %}
+{% load i18n static pretty_money perms %}
 <div class="card bg-light shadow">
     <div class="card-header text-center">
         <h4> Club {{ club.name }} </h4>
@@ -40,9 +40,12 @@
         </dl>
     </div>
     <div class="card-footer text-center">
-        <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a>
-        <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
-        <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a>
+        {% if can_add_members %}
+            <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a>
+        {% endif %}
+        {% if ".change_"|has_perm:club %}
+            <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
+        {% endif %}
         {% url 'member:club_detail' club.pk as club_detail_url %}
         {%if request.get_full_path != club_detail_url %}
         <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>