diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py
index 12530c6e22d4ea0eb86b46d018ffbcddb0c44f8c..265870a85aadd70d949f13cdeb990db5c0955a18 100644
--- a/apps/note/templatetags/pretty_money.py
+++ b/apps/note/templatetags/pretty_money.py
@@ -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,
diff --git a/apps/note/views.py b/apps/note/views.py
index 82f2f4aafbc0ad255e556ccc5e4b8d2ff2021565..f950fd7312d585c05b8c05e92ffcfa2634f26cab 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -141,7 +141,7 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
         """
         context = super().get_context_data(**kwargs)
         context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
-            .order_by('category').order_by('name')
+            .order_by('category__name', 'name')
         context['title'] = _("Consumptions")
         context['polymorphic_ctype'] = ContentType.objects.get_for_model(TemplateTransaction).pk
 
diff --git a/apps/scripts b/apps/scripts
deleted file mode 160000
index 123466cfa914422422cd372197e64adf65ef05f7..0000000000000000000000000000000000000000
--- a/apps/scripts
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 123466cfa914422422cd372197e64adf65ef05f7
diff --git a/static/js/base.js b/static/js/base.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2ad87c99931550a523f35bb622855f54abdb00c
--- /dev/null
+++ b/static/js/base.js
@@ -0,0 +1,54 @@
+// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+/**
+ * Perform a request on an URL and get result
+ * @param url The url where the request is performed
+ * @param success The function to call with the request
+ * @param data The data for the request (optional)
+ */
+function getJSONSync(url, success, data) {
+    $.ajax({
+        url: url,
+        dataType: 'json',
+        data: data,
+        async: false,
+        success: success
+    });
+}
+
+/**
+ * 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) + " €";
+}
+
+/**
+ * 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.
+ * This function is synchronous.
+ */
+function getMatchedNotes(pattern, fun) {
+    getJSONSync("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club", function(aliases) {
+        aliases.results.forEach(function(alias) {
+            getJSONSync("/api/note/note/" + alias.note + "/?format=json", function (note) {
+                fun(note, alias);
+                console.log(alias.name);
+            });
+        });
+    });
+}
diff --git a/static/js/consos.js b/static/js/consos.js
new file mode 100644
index 0000000000000000000000000000000000000000..d3075eddd2a4c52c0f86976740018d35f50faf22
--- /dev/null
+++ b/static/js/consos.js
@@ -0,0 +1,227 @@
+// 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");
+}
+
+$(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");
+    });
+});
+
+let old_pattern = null;
+let notes = [];
+let notes_display = [];
+let buttons = [];
+
+// When the user clicks on the search field, it is immediately cleared
+let note_obj = $("#note");
+note_obj.click(function() {
+    note_obj.val("");
+});
+
+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);
+}
+
+function remove_conso(c, obj, note_prefix="note") {
+    return (function() {
+        let new_notes_display = [];
+        let html = "";
+        notes_display.forEach(function (disp) {
+            if (disp[3] > 1 || disp[1] !== c[1]) {
+                disp[3] -= disp[1] === c[1] ? 1 : 0;
+                new_notes_display = new_notes_display.concat([disp]);
+                html += li(note_prefix + "_" + disp[1], disp[0]
+                    + "<span class=\"badge badge-dark badge-pill\">" + disp[3] + "</span>");
+            }
+        });
+        $("#note_list").html(html);
+        notes_display.forEach(function (disp) {
+            obj = $("#" + note_prefix + "_" + disp[1]);
+            obj.click(remove_conso(disp, obj, note_prefix));
+            obj.hover(function() {
+                displayNote(disp[2], disp[0]);
+            });
+        });
+        notes_display = new_notes_display;
+    });
+}
+
+
+function autoCompleteNote(field_id, alias_matched_id, alias_prefix="alias", note_prefix="note",
+                          user_note_field=null, profile_pic_field=null) {
+    let field = $("#" + field_id);
+    field.keyup(function() {
+        let pattern = field.val();
+        // If the pattern is not modified, or if the field is empty, we don't query the API
+        if (pattern === old_pattern || pattern === "")
+            return;
+
+        old_pattern = pattern;
+
+        notes = [];
+        let aliases_matched_obj = $("#" + alias_matched_id);
+        let aliases_matched_html = "";
+        getMatchedNotes(pattern, function(note, alias) {
+            aliases_matched_html += li("alias_" + alias.normalized_name, alias.name);
+            note.alias = alias;
+            notes = notes.concat([note]);
+        });
+
+        aliases_matched_obj.html(aliases_matched_html);
+
+        notes.forEach(function (note) {
+            let alias = note.alias;
+            let alias_obj = $("#" + alias_prefix + "_" + alias.normalized_name);
+            alias_obj.hover(function() {
+                displayNote(note, alias.name, user_note_field, profile_pic_field);
+            });
+
+            alias_obj.click(function() {
+                field.val("");
+                var disp = null;
+                notes_display.forEach(function(d) {
+                    if (d[1] === note.id) {
+                        d[3] += 1;
+                        disp = d;
+                    }
+                });
+                if (disp == null)
+                    notes_display = notes_display.concat([[alias.name, note.id, note, 1]]);
+                let note_list = $("#note_list");
+                let html = "";
+                notes_display.forEach(function(disp) {
+                   html += li("note_" + disp[1], disp[0]
+                        + "<span class=\"badge badge-dark badge-pill\">" + disp[3] + "</span>");
+                });
+
+                note_list.html(html);
+
+                notes_display.forEach(function(disp) {
+                    let line_obj = $("#" + note_prefix + "_" + disp[1]);
+                    line_obj.hover(function() {
+                        displayNote(disp[2], disp[0], user_note_field, profile_pic_field);
+                    });
+
+                    line_obj.click(remove_conso(disp, note_prefix));
+                });
+            });
+        });
+    });
+}
+
+
+// When the user searches an alias, we update the auto-completion
+autoCompleteNote("note", "alias_matched", "alias", "note",
+    "user_note", "profile_pic");
+
+/**
+ * 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[5] === template_id) {
+            b[1] += 1;
+            button = b;
+        }
+    });
+    if (button == null)
+        buttons = buttons.concat([[dest, 1, amount, type, category_id, category_name, template_id, template_name]]);
+
+    // TODO Only in simple consumption mode
+    if (notes.length > 0)
+        consumeAll();
+}
+
+/**
+ * Apply all transactions: all notes in `notes` buy each item in `buttons`
+ */
+function consumeAll() {
+    notes.forEach(function(note) {
+        buttons.forEach(function(button) {
+            consume(note.id, button[0], button[1], button[2], button[7] + " (" + button[5] + ")",
+                button[3], button[4], button[6]);
+       });
+    });
+}
+
+/**
+ * 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
+        }, function() {
+            notes_display = [];
+            notes = [];
+            buttons = [];
+            old_pattern = null;
+            $("#note_list").html("");
+            $("#alias_matched").html("");
+            displayNote(null, "");
+            refreshHistory();
+            refreshBalance();
+        });
+}
diff --git a/templates/base.html b/templates/base.html
index 43f1ae5f6764306a5f8f96ceb0f3cef846e2c1de..2f2f4ab4fb7ad5aff902e36a49b0fd1ed8cf24e6 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -46,6 +46,7 @@ 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 %}
diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html
index 642051082324b04bab11567a1d6f29fd6209f097..9b096df31a4aacc819d29a7d1229f324e0d190cd 100644
--- a/templates/note/conso_form.html
+++ b/templates/note/conso_form.html
@@ -25,7 +25,7 @@
                     <div class="card border-success shadow mb-4">
                         <div class="card-header">
                             <p class="card-text font-weight-bold">
-                                Sélection des émitteurs
+                                Sélection des émetteurs
                             </p>
                         </div>
                         <ul class="list-group list-group-flush" id="note_list">
@@ -125,184 +125,21 @@
             min-width: 100%;
         }
     </style>
-
-    <link href="/static/vendor/select2/dist/css/select2.css" type="text/css" media="screen" rel="stylesheet">
-    <link href="/static/admin/css/autocomplete.css" type="text/css" media="screen" rel="stylesheet">
-    <link href="/static/autocomplete_light/select2.css" type="text/css" media="screen" rel="stylesheet">
-    <script type="text/javascript" src="/static/autocomplete_light/jquery.init.js"></script>
-    <script type="text/javascript" src="/static/vendor/select2/dist/js/select2.full.js"></script>
-    <script type="text/javascript" src="/static/vendor/select2/dist/js/i18n/fr.js"></script>
-    <script type="text/javascript" src="/static/autocomplete_light/autocomplete.init.js"></script>
-    <script type="text/javascript" src="/static/autocomplete_light/forward.js"></script>
-    <script type="text/javascript" src="/static/autocomplete_light/select2.js"></script>
-    <script type="text/javascript" src="/static/autocomplete_light/jquery.post-setup.js"></script>
 {% endblock %}
 
 {% block extrajavascript %}
+    <script type="text/javascript" src="/static/js/consos.js"></script>
     <script type="text/javascript">
-        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);
-        }
-
-        let old_pattern = null;
-        let notes = [];
-        let consos = [];
-
-        $(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(event) {
-                location.hash = this.getAttribute("href");
-            });
-
-            let note_obj = $("#note");
-            note_obj.click(function() {
-                note_obj.val("");
-            });
-            note_obj.keyup(function() {
-                let pattern = note_obj.val();
-                if (pattern === old_pattern || pattern === "")
-                    return;
-
-                old_pattern = pattern;
-
-                notes = [];
-                let aliases_matched_obj = $("#alias_matched");
-                let aliases_matched_html = "";
-                $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club", function(aliases) {
-                    aliases.results.forEach(function(alias) {
-                        aliases_matched_html += "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" +
-                            " id=\"alias_" + alias.normalized_name + "\">" + alias.name + "</li>\n";
-                        $.getJSON("/api/note/note/" + alias.note + "/?format=json", function(note) {
-                            notes += note;
-                            let alias_obj = $("#alias_" + alias.normalized_name);
-                            alias_obj.hover(function() {
-                                var name = alias.name;
-                                if (name !== note.name)
-                                    name += " (aka. " + note.name + ")";
-                                $("#user_note").text(name + " : " + pretty_money(note.balance));
-                            if (note.display_image == null)
-                                $("#profile_pic").attr('src', '/media/pic/default.png');
-                            else
-                                $("#profile_pic").attr('src', note.display_image);
-                            });
-
-                            alias_obj.click(function() {
-                                note_obj.val("");
-                                var conso = null;
-                                consos.forEach(function(c) {
-                                    if (c[1] === note.id) {
-                                        c[3] += 1;
-                                        conso = c;
-                                    }
-                                });
-                                if (conso == null)
-                                    consos = consos.concat([[alias.name, note.id, note, 1]]);
-                                let note_list = $("#note_list");
-                                let html = "";
-                                consos.forEach(function(conso) {
-                                   html += "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" +
-                                       " id=\"note_" + conso[1] + "\">" + conso[0] + "<span class=\"badge badge-dark badge-pill\">"
-                                       + conso[3] + "</span></li>\n";
-                                });
-
-                                note_list.html(html);
-
-                                consos.forEach(function(conso) {
-                                    let line_obj = $("#note_" + conso[1]);
-                                    line_obj.hover(function() {
-                                        $("#user_note").text(conso[0] + " : " + pretty_money(conso[2].balance));
-                                        if (conso[2].display_image == null)
-                                            $("#profile_pic").attr('src', '/media/pic/default.png');
-                                        else
-                                            $("#profile_pic").attr('src', conso[2].display_image);
-                                    });
-
-                                    function line_obj_click(c) {
-                                        return (function() {
-                                            let new_consos = [];
-                                            let html = "";
-                                            consos.forEach(function (conso) {
-                                                if (conso[3] > 1 || conso[1] !== c[1]) {
-                                                    conso[3] -= conso[1] === c[1] ? 1 : 0;
-                                                    new_consos = new_consos.concat([conso]);
-                                                    html += "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" +
-                                                        " id=\"note_" + conso[1] + "\">" + conso[0] + "<span class=\"badge badge-dark badge-pill\">"
-                                                        + conso[3] + "</span></li>\n";
-                                                }
-                                            });
-                                            note_list.html(html);
-                                            consos.forEach(function (conso) {
-                                                $("#note_" + conso[1]).click(line_obj_click(conso));
-                                                line_obj.hover(function() {
-                                                    $("#user_note").text(conso[0] + " : " + pretty_money(conso[2].balance));
-                                                    if (conso[2].display_image == null)
-                                                        $("#profile_pic").attr('src', '/media/pic/default.png');
-                                                    else
-                                                        $("#profile_pic").attr('src', conso[2].display_image);
-                                                });
-                                            });
-                                            consos = new_consos;
-                                        });
-                                    }
-
-                                    line_obj.click(line_obj_click(conso));
-                                });
-                            });
-                        });
-                    });
-                    aliases_matched_obj.html(aliases_matched_html);
+        let CSRF_TOKEN = "{{ csrf_token }}";
+
+        {% 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 }}");
                 });
-            });
-
-            {% for button in transaction_templates %}
-                {% if button.display %}
-                    $("#button{{ button.id }}").click(function() {
-                        consos.forEach(function(conso) {
-                            $.post("/api/note/transaction/transaction/",
-                            {
-                                "csrfmiddlewaretoken": "{{ csrf_token }}",
-                                "quantity": conso[3],
-                                "amount": {{ button.amount }},
-                                "reason": "{{ button.name }} ({{ button.category.name }})",
-                                "valid": true,
-                                "polymorphic_ctype": {{ polymorphic_ctype }},
-                                "resourcetype": "TemplateTransaction",
-                                "source": conso[1],
-                                "destination": {{ button.destination.pk }},
-                                "category": {{ button.category.id }},
-                                "template": {{ button.id }}
-                            }, function () {
-                                consos = [];
-                                $("#note_list").html("");
-                                $("#alias_matched").html("");
-                                $("#profile_pic").attr("src", "/media/pic/default.png");
-                                $("#user_note").text("");
-                                refreshHistory();
-                                refreshBalance();
-                            });
-                        });
-                    });
-                {% endif %}
-            {% endfor %}
-        });
-
-        function refreshBalance() {
-            $("#user_balance").load("/ #user_balance");
-        }
-
-        function refreshHistory() {
-            $("#history").load("/note/consos/ #history");
-        }
+            {% endif %}
+        {% endfor %}
     </script>
 {% endblock %}