diff --git a/apps/member/hashers.py b/apps/member/hashers.py
new file mode 100644
index 0000000000000000000000000000000000000000..0c5d010b62f22fc45c1140866aad46df05ebae32
--- /dev/null
+++ b/apps/member/hashers.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+import hashlib
+
+from django.contrib.auth.hashers import PBKDF2PasswordHasher
+from django.utils.crypto import constant_time_compare
+
+
+class CustomNK15Hasher(PBKDF2PasswordHasher):
+    """
+    Permet d'importer les mots de passe depuis la Note KFet 2015.
+    Si un hash de mot de passe est de la forme :
+    `custom_nk15$<NB>$<ENCODED>`
+    où <NB> est un entier quelconque (symbolisant normalement un nombre d'itérations)
+    et <ENCODED> le hash du mot de passe dans la Note Kfet 2015,
+    alors ce hasher va vérifier le mot de passe.
+    N'ayant pas la priorité (cf note_kfet/settings/base.py), le mot de passe sera
+    converti automatiquement avec l'algorithme PBKDF2.
+    """
+    algorithm = "custom_nk15"
+
+    def verify(self, password, encoded):
+        if '|' in encoded:
+            salt, db_hashed_pass = encoded.split('$')[2].split('|')
+            return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
+        return super().verify(password, encoded)
diff --git a/apps/member/management/__init__.py b/apps/member/management/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/apps/member/management/commands/__init__.py b/apps/member/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/apps/member/management/commands/import_nk15.py b/apps/member/management/commands/import_nk15.py
new file mode 100644
index 0000000000000000000000000000000000000000..4b8d57aa15464a41639bc04b421b77c145302664
--- /dev/null
+++ b/apps/member/management/commands/import_nk15.py
@@ -0,0 +1,216 @@
+#!/usr/env/bin python3
+
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+import psycopg2 as  pg
+import psycopg2.extras as pge
+from django.db import transaction
+
+import collections
+
+from django.core.exceptions import ValidationError
+from django.db import IntegrityError
+from django.contrib.auth.models import User
+from note.models import Note, NoteSpecial, NoteUser, NoteClub
+from note.models import Alias
+from note.models import Transaction, TransactionTemplate, TemplateCategory, TransactionType
+from member.models import Profile, Club
+
+"""
+Script d'import de la nk15:
+TODO: import aliases
+TODO: import transactions
+TODO: import adhesion
+TODO: import activite
+TODO: import
+
+"""
+@transaction.atomic
+def import_special(cur):
+    cur.execute("SELECT * FROM comptes WHERE idbde <0 ORDER BY idbde;")
+    map_idbde = dict()
+    for row in cur:
+        obj,created = NoteSpecial.objects.get_or_create(special_type = row["pseudo"],
+                                                        balance = row["solde"],
+                                                        is_active =True)
+        if created:
+            obj.save()
+            map_idbde[row["idbde"]] = obj.pk
+
+    cur.execute("SELECT * FROM comptes WHERE idbde=0;")
+    res = cur.fetchone()
+    clubBde, c = Club.objects.get_or_create(pk = 1,
+                                            name = "Bde",
+                                            email = "bureau.bde@lists.crans.org",
+                                            membership_duration = "396 00:00:00",
+                                            membership_start = "213 00:00:00",
+                                            membership_end = "273 00:00:00",
+                                            membership_fee = 5,
+    )
+    clubKfet, c = Club.objects.get_or_create(pk = 2,
+                                             name = "Kfet",
+                                             email = "tresorerie.bde@lists.crans.org",
+                                             membership_duration = "396 00:00:00",
+                                             membership_start = "213 00:00:00",
+                                             membership_end = "273 00:00:00",
+                                             membership_fee = 35,
+    )
+    clubBde.save()
+    clubKfet.save()
+    clubBde.note.solde=res["solde"]
+    map_idbde[0] = clubKfet.note.pk
+    return map_idbde
+
+
+@transaction.atomic
+def import_comptes(cur,map_idbde):
+    cur.execute("SELECT * FROM comptes WHERE idbde > 0 ORDER BY idbde;")
+    pkclub = 3
+    for row in cur:
+        if row["type"] == "personne":
+            #sanitize password
+            if row["passwd"] != "*|*":
+                passwd_nk15 = "$".join(["custom_nk15","1",row["passwd"]])
+            else:
+                passwd_nk15 = ''
+            try:
+                obj_dict = {
+                    "username": row["pseudo"],
+                    "password": passwd_nk15,
+                    "first_name": row["nom"],
+                    "last_name": row["prenom"],
+                    "email":  row["mail"],
+                    "is_active" : False, # temporary
+                }
+                user = User.objects.create(**obj_dict)
+               #sanitize duplicate aliases (nk12)
+            except ValidationError as e:
+                if e.code == 'same_alias':
+                    obj_dict["username"] = row["pseudo"]+str(row["idbde"])
+                    user = User.objects.create(**obj_dict)
+                else:
+                    raise(e)
+            else:
+                pass
+            obj_dict ={
+                "phone_number": row["tel"],
+                "address":  row["adresse"],
+                "paid": row["normalien"],
+                "user": user,
+            }
+            profile = Profile.objects.create(**obj_dict)
+            note = user.note
+            note.balance = row["solde"]
+            obj_list =[user, profile, note]
+        else: # club
+            obj_dict = {
+                "pk":pkclub,
+                "name": row["pseudo"],
+                "email": row["mail"],
+                "membership_duration": "396 00:00:00",
+                "membership_start": "213 00:00:00",
+                "membership_end": "273 00:00:00",
+                "membership_fee": 0,
+            }
+            club,c = Club.objects.get_or_create(**obj_dict)
+            pkclub +=1
+            note = club.note
+            note.balance = row["solde"]
+            obj_list = [club,note]
+        for obj in obj_list:
+            obj.save()
+            map_idbde[row["idbde"]] = note.pk
+    return map_idbde
+
+
+@transaction.atomic
+def import_boutons(cur,map_idbde):
+    cur.execute("SELECT * FROM boutons;")
+    for row in cur:
+        cat, created = TemplateCategory.objects.get_or_create(name=row["categorie"])
+        obj_dict = {
+            "pk": row["id"],
+            "name": row["label"],
+            "amount": row["montant"],
+            "destination_id": map_idbde[row["destinataire"]],
+            "category": cat,
+            "display" : row["affiche"],
+            "description": row["description"],
+        }
+        try:
+            with transaction.atomic(): # required for error management
+                button = TransactionTemplate.objects.create(**obj_dict)
+        except IntegrityError as e:
+            if "unique" in e.args[0]:
+                qs = Club.objects.filter(note__id=map_idbde[row["destinataire"]]).values('name')
+                note_name = qs[0]["name"]
+                obj_dict["name"] = ' '.join([obj_dict["name"],note_name])
+                button = TransactionTemplate.objects.create(**obj_dict)
+            else:
+                raise(e)
+        if created:
+            cat.save()
+        button.save()
+
+
+@transaction.atomic
+def import_transaction(cur, map_idbde):
+    cur.execute("SELECT * FROM transactions;")
+    for row in cur:
+        obj_dict = {
+            "pk":row["id"],
+        }
+       
+@transaction.atomic
+def import_aliases(cur,map_idbde):
+    cur.execute("SELECT * FROM aliases ORDER by id")
+    for row in cur:
+        alias_name = row["alias"]
+        alias_name_good = (alias_name[:252]+'...') if len(alias_name) > 255 else alias_name
+        obj_dict = {
+            "note_id":map_idbde[row["idbde"]],
+            "name":alias_name_good,
+        }
+        try:
+            with transaction.atomic():
+                alias =  Alias.objects.create(**obj_dict)
+        except IntegrityError as e:
+            if "unique" in e.args[0]:
+                continue
+            else:
+                raise(e)
+        alias.save()
+
+
+class Command(BaseCommand):
+    """
+    Command for importing the database of NK15.
+    Need to be run by a user with a registered role in postgres for the database nk15. 
+    """
+    def add_arguments(self,parser):
+        parser.add_argument('-s', '--special', action = 'store_true')
+        parser.add_argument('-c', '--comptes', action = 'store_true')
+        parser.add_argument('-b', '--boutons', action = 'store_true')
+        parser.add_argument('-t', '--transactions', action = 'store_true')
+        parser.add_argument('-a', '--aliases', action = 'store_true')
+       
+    def handle(self, *args, **kwargs):
+        conn = pg.connect(database="nk15",user="nk15_user")
+        cur = conn.cursor(cursor_factory = pge.DictCursor)
+
+        if kwargs["special"]:
+            map_idbde = import_special(cur)
+            print("Minimal setup created")
+
+        if kwargs["comptes"]:
+            map_idbde = import_comptes(cur,map_idbde)
+            print("comptes table imported")
+
+        if kwargs["boutons"]:
+            import_boutons(cur,map_idbde)
+            print("boutons table imported")
+        if kwargs["transactions"]:
+            import_transaction(cur)
+        if kwargs["aliases"]:
+            import_aliases(cur,map_idbde)
+            print("aliases imported")
diff --git a/apps/note/admin.py b/apps/note/admin.py
index 3a9721aeee3eaa7c30750ee4a31109a97fb6f5cb..52c1cc1752066221c27782a303e975e497d278cf 100644
--- a/apps/note/admin.py
+++ b/apps/note/admin.py
@@ -7,7 +7,8 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
     PolymorphicChildModelFilter, PolymorphicParentModelAdmin
 
 from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
-from .models.transactions import Transaction, TransactionCategory, TransactionTemplate
+from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
+    TemplateTransaction, MembershipTransaction
 
 
 class AliasInlines(admin.TabularInline):
@@ -97,13 +98,14 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
 
 
 @admin.register(Transaction)
-class TransactionAdmin(admin.ModelAdmin):
+class TransactionAdmin(PolymorphicParentModelAdmin):
     """
     Admin customisation for Transaction
     """
+    child_models = (TemplateTransaction, MembershipTransaction)
     list_display = ('created_at', 'poly_source', 'poly_destination',
-                    'quantity', 'amount', 'transaction_type', 'valid')
-    list_filter = ('transaction_type', 'valid')
+                    'quantity', 'amount', 'valid')
+    list_filter = ('valid',)
     autocomplete_fields = (
         'source',
         'destination',
@@ -132,7 +134,7 @@ class TransactionAdmin(admin.ModelAdmin):
         """
         if obj:  # user is editing an existing object
             return 'created_at', 'source', 'destination', 'quantity',\
-                   'amount', 'transaction_type'
+                   'amount'
         return []
 
 
@@ -141,8 +143,8 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
     """
     Admin customisation for TransactionTemplate
     """
-    list_display = ('name', 'poly_destination', 'amount', 'template_type')
-    list_filter = ('template_type', )
+    list_display = ('name', 'poly_destination', 'amount', 'category', 'display', )
+    list_filter = ('category', 'display')
     autocomplete_fields = ('destination', )
 
     def poly_destination(self, obj):
@@ -154,8 +156,8 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
     poly_destination.short_description = _('destination')
 
 
-@admin.register(TransactionCategory)
-class TransactionCategoryAdmin(admin.ModelAdmin):
+@admin.register(TemplateCategory)
+class TemplateCategoryAdmin(admin.ModelAdmin):
     """
     Admin customisation for TransactionTemplate
     """
diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json
index f853d3cbb0ec8c248162c65bacc033cabdde9fb9..c0e92bda3aa9c6d2d7be0193b3b70b0df3300c0b 100644
--- a/apps/note/fixtures/initial.json
+++ b/apps/note/fixtures/initial.json
@@ -162,59 +162,59 @@
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 1,
         "fields": {
             "name": "Soft"
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 2,
         "fields": {
             "name": "Pulls"
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 3,
         "fields": {
             "name": "Gala"
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 4,
         "fields": {
             "name": "Clubs"
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 5,
         "fields": {
             "name": "Bouffe"
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 6,
         "fields": {
             "name": "BDA"
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 7,
         "fields": {
             "name": "Autre"
         }
     },
     {
-        "model": "note.transactioncategory",
+        "model": "note.templatecategory",
         "pk": 8,
         "fields": {
             "name": "Alcool"
         }
     }
-]
\ No newline at end of file
+]
diff --git a/apps/note/forms.py b/apps/note/forms.py
index e4fd344c1672031f4bf57279de23c6d965c32ffc..2f49d88fb54fce52aee47cd905cb733430b23ca9 100644
--- a/apps/note/forms.py
+++ b/apps/note/forms.py
@@ -4,7 +4,7 @@
 from dal import autocomplete
 from django import forms
 
-from .models import Transaction, TransactionTemplate
+from .models import Transaction, TransactionTemplate, TemplateTransaction
 
 
 class TransactionTemplateForm(forms.ModelForm):
@@ -71,12 +71,13 @@ class ConsoForm(forms.ModelForm):
             name=self.data['button']).get()
         self.instance.destination = button.destination
         self.instance.amount = button.amount
-        self.instance.transaction_type = 'bouton'
-        self.instance.reason = button.name
+        self.instance.reason = '{} ({})'.format(button.name, button.category)
+        self.instance.name = button.name
+        self.instance.category = button.category
         super().save(commit)
 
     class Meta:
-        model = Transaction
+        model = TemplateTransaction
         fields = ('source', )
 
         # Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py
index 7e6cc310e823e28eea3f8d8e40b8e1d98b87ac7c..081b31a737f6048c562773cbb67c51aca442b8bd 100644
--- a/apps/note/models/__init__.py
+++ b/apps/note/models/__init__.py
@@ -3,11 +3,12 @@
 
 from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
 from .transactions import MembershipTransaction, Transaction, \
-    TransactionCategory, TransactionTemplate
+    TemplateCategory, TransactionTemplate, TemplateTransaction
 
 __all__ = [
     # Notes
     'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
     # Transactions
-    'MembershipTransaction', 'Transaction', 'TransactionCategory', 'TransactionTemplate',
+    'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
+    'TemplateTransaction',
 ]
diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py
index 3b616f0e754abaee90a917ae0288d0410ce1de77..3d929bc82eb10e5739f6fed0fba7182879b7ef1f 100644
--- a/apps/note/models/notes.py
+++ b/apps/note/models/notes.py
@@ -64,7 +64,8 @@ class Note(PolymorphicModel):
         if aliases.exists():
             # Alias exists, so check if it is linked to this note
             if aliases.first().note != self:
-                raise ValidationError(_('This alias is already taken.'))
+                raise ValidationError(_('This alias is already taken.'),
+                                      code="same_alias")
 
             # Save note
             super().save(*args, **kwargs)
@@ -87,7 +88,8 @@ class Note(PolymorphicModel):
         if aliases.exists():
             # Alias exists, so check if it is linked to this note
             if aliases.first().note != self:
-                raise ValidationError(_('This alias is already taken.'))
+                raise ValidationError(_('This alias is already taken.'),
+                                      code="same_alias",)
         else:
             # Alias does not exist yet, so check if it can exist
             a = Alias(name=str(self))
@@ -222,16 +224,19 @@ class Alias(models.Model):
     def clean(self):
         normalized_name = Alias.normalize(self.name)
         if len(normalized_name) >= 255:
-            raise ValidationError(_('Alias too long.'))
+            raise ValidationError(_('Alias is too long.'),
+                                  code='alias_too_long')
         try:
-            if self != Alias.objects.get(normalized_name=normalized_name):
-                raise ValidationError(
-                    _('An alias with a similar name '
-                      'already exists.'))
+            sim_alias = Alias.objects.get(normalized_name=normalized_name)
+            if self != sim_alias:
+                raise ValidationError(_('An alias with a similar name already exists:'),
+                                       code="same_alias"
+                )
         except Alias.DoesNotExist:
             pass
 
     def delete(self, using=None, keep_parents=False):
         if self.name == str(self.note):
-            raise ValidationError(_("You can't delete your main alias."))
+            raise ValidationError(_("You can't delete your main alias."),
+                                  code="cant_delete_main_alias")
         return super().delete(using, keep_parents)
diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py
index 042faa161184e058b706d3e478eda816a255e9aa..c99b55384dc84bf532740845707e54f3c5c62b8e 100644
--- a/apps/note/models/transactions.py
+++ b/apps/note/models/transactions.py
@@ -5,6 +5,7 @@ from django.db import models
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 from django.urls import reverse
+from polymorphic.models import PolymorphicModel
 
 from .notes import Note, NoteClub
 
@@ -13,7 +14,7 @@ Defines transactions
 """
 
 
-class TransactionCategory(models.Model):
+class TemplateCategory(models.Model):
     """
     Defined a recurrent transaction category
 
@@ -43,6 +44,7 @@ class TransactionTemplate(models.Model):
         verbose_name=_('name'),
         max_length=255,
         unique=True,
+        error_messages={'unique':_("A template with this name already exist")},
     )
     destination = models.ForeignKey(
         NoteClub,
@@ -54,12 +56,19 @@ class TransactionTemplate(models.Model):
         verbose_name=_('amount'),
         help_text=_('in centimes'),
     )
-    template_type = models.ForeignKey(
-        TransactionCategory,
+    category = models.ForeignKey(
+        TemplateCategory,
         on_delete=models.PROTECT,
         verbose_name=_('type'),
         max_length=31,
     )
+    display = models.BooleanField(
+        default = True,
+    )
+    description = models.CharField(
+        verbose_name=_('description'),
+        max_length=255,
+    )
 
     class Meta:
         verbose_name = _("transaction template")
@@ -69,7 +78,7 @@ class TransactionTemplate(models.Model):
         return reverse('note:template_update', args=(self.pk, ))
 
 
-class Transaction(models.Model):
+class Transaction(PolymorphicModel):
     """
     General transaction between two :model:`note.Note`
 
@@ -100,10 +109,6 @@ class Transaction(models.Model):
         default=1,
     )
     amount = models.PositiveIntegerField(verbose_name=_('amount'), )
-    transaction_type = models.CharField(
-        verbose_name=_('type'),
-        max_length=31,
-    )
     reason = models.CharField(
         verbose_name=_('reason'),
         max_length=255,
@@ -144,6 +149,26 @@ class Transaction(models.Model):
         return self.amount * self.quantity
 
 
+class TemplateTransaction(Transaction):
+    """
+    Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
+
+    """
+
+    template = models.ForeignKey(
+        TransactionTemplate,
+        null=True,
+        on_delete=models.SET_NULL,
+    )
+    category = models.ForeignKey(
+        TemplateCategory,
+        on_delete=models.PROTECT,
+    )
+    name = models.CharField(
+        max_length=255,
+    )
+
+
 class MembershipTransaction(Transaction):
     """
     Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
diff --git a/apps/note/views.py b/apps/note/views.py
index 167ef4f0d4c4c8a45c251e7e60af2ebb622b98b1..75577a2e3ced39bbd28b165b5b5d3f6438d4552c 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -8,7 +8,7 @@ from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import CreateView, ListView, UpdateView
 
-from .models import Transaction, TransactionTemplate, Alias
+from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction
 from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
 
 
@@ -129,7 +129,7 @@ class ConsoView(LoginRequiredMixin, CreateView):
     """
     Consume
     """
-    model = Transaction
+    model = TemplateTransaction
     template_name = "note/conso_form.html"
     form_class = ConsoForm
 
@@ -138,8 +138,8 @@ class ConsoView(LoginRequiredMixin, CreateView):
         Add some context variables in template such as page title
         """
         context = super().get_context_data(**kwargs)
-        context['transaction_templates'] = TransactionTemplate.objects.all() \
-            .order_by('template_type')
+        context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
+            .order_by('category')
         context['title'] = _("Consommations")
 
         # select2 compatibility
@@ -152,3 +152,4 @@ class ConsoView(LoginRequiredMixin, CreateView):
         When clicking a button, reload the same page
         """
         return reverse('note:consos')
+
diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py
index ab4453e406eb2f00c219b87891b531b7a85b3bdc..a58d4817df2bfa86462ca5283cbdaea58073964e 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -117,6 +117,12 @@ AUTH_PASSWORD_VALIDATORS = [
     },
 ]
 
+# Use our custom hasher in order to import NK15 passwords
+PASSWORD_HASHERS = [
+    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+    'member.hashers.CustomNK15Hasher',
+]
+
 # Django Guardian object permissions
 
 AUTHENTICATION_BACKENDS = (
diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html
index b121ad54d12f970a66f9572194d3b45d8a3376c8..10b06589cfd4434aa6bc79100307901d017b1547 100644
--- a/templates/note/conso_form.html
+++ b/templates/note/conso_form.html
@@ -7,7 +7,7 @@
 
 {% block content %}
     {# Regroup buttons under categories #}
-    {% regroup transaction_templates by template_type as template_types %}
+    {% regroup transaction_templates by category as categories %}
 
     <form method="post" onsubmit="window.onbeforeunload=null">
         {% csrf_token %}
@@ -44,10 +44,10 @@
                     {# 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" data-toggle="tab" href="#{{ template_type.grouper|slugify }}">
-                                        {{ template_type.grouper }}
+                                    <a class="nav-link" data-toggle="tab" href="#{{ category.grouper|slugify }}">
+                                        {{ category.grouper }}
                                     </a>
                                 </li>
                             {% endfor %}
@@ -57,10 +57,10 @@
                     {# 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 %}
                                             <button class="btn btn-outline-dark rounded-0 flex-fill"
                                                     name="button" value="{{ button.name }}">
                                                 {{ button.name }} ({{ button.amount | pretty_money }})