From 7b69ffdefe302963dbdc702c3d44fcda8c96e39c Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO <yohann.danello@gmail.com>
Date: Thu, 12 Mar 2020 23:12:49 +0100
Subject: [PATCH] Better transfers

---
 apps/note/forms.py                   |  49 ---------
 apps/note/views.py                   |  28 +----
 locale/fr/LC_MESSAGES/django.po      |   7 ++
 static/js/base.js                    |  26 +++--
 templates/base.html                  |   1 +
 templates/note/conso_form.html       |   2 +-
 templates/note/transaction_form.html | 155 ++++++++++++++++++++++-----
 7 files changed, 158 insertions(+), 110 deletions(-)

diff --git a/apps/note/forms.py b/apps/note/forms.py
index 2e8e4456..933e29f5 100644
--- a/apps/note/forms.py
+++ b/apps/note/forms.py
@@ -50,52 +50,3 @@ class TransactionTemplateForm(forms.ModelForm):
                     },
                 ),
         }
-
-
-class TransactionForm(forms.ModelForm):
-    def save(self, commit=True):
-        super().save(commit)
-
-    def clean(self):
-        """
-        If the user has no right to transfer funds, then it will be the source of the transfer by default.
-        Transactions between a note and the same note are not authorized.
-        """
-
-        cleaned_data = super().clean()
-        if "source" not in cleaned_data:  # TODO Replace it with "if %user has no right to transfer funds"
-            cleaned_data["source"] = self.user.note
-
-        if cleaned_data["source"].pk == cleaned_data["destination"].pk:
-            self.add_error("destination", _("Source and destination must be different."))
-
-        return cleaned_data
-
-    class Meta:
-        model = Transaction
-        fields = (
-            'source',
-            'destination',
-            'reason',
-            'amount',
-        )
-
-        # Voir ci-dessus
-        widgets = {
-            'source':
-                autocomplete.ModelSelect2(
-                    url='note:note_autocomplete',
-                    attrs={
-                        'data-placeholder': 'Note ...',
-                        'data-minimum-input-length': 1,
-                    },
-                ),
-            'destination':
-                autocomplete.ModelSelect2(
-                    url='note:note_autocomplete',
-                    attrs={
-                        'data-placeholder': 'Note ...',
-                        'data-minimum-input-length': 1,
-                    },
-                ),
-        }
diff --git a/apps/note/views.py b/apps/note/views.py
index f950fd73..e323a4fd 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -5,24 +5,22 @@ from dal import autocomplete
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
-from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
-from django.views.generic import CreateView, ListView, UpdateView
+from django.views.generic import CreateView, ListView, UpdateView, TemplateView
 from django_tables2 import SingleTableView
 
-from .forms import TransactionForm, TransactionTemplateForm
+from .forms import TransactionTemplateForm
 from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction
 from .tables import HistoryTable
 
 
-class TransactionCreate(LoginRequiredMixin, CreateView):
+class TransactionCreate(LoginRequiredMixin, TemplateView):
     """
     Show transfer page
 
     TODO: If user have sufficient rights, they can transfer from an other note
     """
-    model = Transaction
-    form_class = TransactionForm
+    template_name = "note/transaction_form.html"
 
     def get_context_data(self, **kwargs):
         """
@@ -31,26 +29,10 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
         context = super().get_context_data(**kwargs)
         context['title'] = _('Transfer money from your account '
                              'to one or others')
-
-        context['no_cache'] = True
+        context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
 
         return context
 
-    def get_form(self, form_class=None):
-        """
-        If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
-        """
-        form = super().get_form(form_class)
-
-        if False:  # TODO: fix it with "if %user has no right to transfer funds"
-            del form.fields['source']
-            form.user = self.request.user
-
-        return form
-
-    def get_success_url(self):
-        return reverse('note:transfer')
-
 
 class NoteAutocomplete(autocomplete.Select2QuerySetView):
     """
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 05836a54..6fb317ad 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -408,6 +408,10 @@ msgstr "Un modèle de transaction avec un nom similaire existe déjà."
 msgid "amount"
 msgstr "montant"
 
+#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
+msgid "Amount"
+msgstr "Montant"
+
 #: apps/note/models/transactions.py:57
 msgid "in centimes"
 msgstr "en centimes"
@@ -428,6 +432,9 @@ msgstr "quantité"
 msgid "reason"
 msgstr "raison"
 
+msgid "Reason"
+msgstr "Raison"
+
 #: apps/note/models/transactions.py:115
 msgid "valid"
 msgstr "valide"
diff --git a/static/js/base.js b/static/js/base.js
index 0c63fb26..cfd8f70e 100644
--- a/static/js/base.js
+++ b/static/js/base.js
@@ -26,7 +26,8 @@ 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) + " €";
+        return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "."
+            + (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €";
 }
 
 /**
@@ -47,7 +48,6 @@ function getMatchedNotes(pattern, fun) {
         aliases.results.forEach(function(alias) {
             getJSONSync("/api/note/note/" + alias.note + "/?format=json", function (note) {
                 fun(note, alias);
-                console.log(alias.name);
             });
         });
     });
@@ -85,13 +85,14 @@ function displayNote(note, alias, user_note_field=null, profile_pic_field=null)
  * @param d The note to remove
  * @param note_prefix The prefix of the identifiers of the <li> blocks of the emitters
  * @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
+ * @param note_list_id The div block identifier where the notes of the buyers are displayed
  * @param user_note_field The identifier of the field that display the note of the hovered note (useful in
  *                        consumptions, put null if not used)
  * @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
  *                          (useful in consumptions, put null if not used)
  * @returns an anonymous function to be compatible with jQuery events
  */
-function removeNote(d, note_prefix="note", notes_display, user_note_field=null, profile_pic_field=null) {
+function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) {
     return (function() {
         let new_notes_display = [];
         let html = "";
@@ -103,15 +104,20 @@ function removeNote(d, note_prefix="note", notes_display, user_note_field=null,
                     + "<span class=\"badge badge-dark badge-pill\">" + disp[3] + "</span>");
             }
         });
-        $("#note_list").html(html);
+
+        notes_display.length = 0;
+        new_notes_display.forEach(function(disp) {
+            notes_display.push(disp);
+        });
+
+        $("#" + note_list_id).html(html);
         notes_display.forEach(function (disp) {
-            obj = $("#" + note_prefix + "_" + disp[1]);
-            obj.click(removeNote(disp, note_prefix, notes_display, user_note_field, profile_pic_field));
+            let obj = $("#" + note_prefix + "_" + disp[1]);
+            obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
             obj.hover(function() {
                 displayNote(disp[2], disp[0], user_note_field, profile_pic_field);
             });
         });
-        notes_display = new_notes_display;
     });
 }
 
@@ -155,7 +161,7 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes
         let aliases_matched_html = "";
         // Get matched notes with the given pattern
         getMatchedNotes(pattern, function(note, alias) {
-            aliases_matched_html += li("alias_" + alias.normalized_name, alias.name);
+            aliases_matched_html += li(alias_prefix + "_" + alias.normalized_name, alias.name);
             note.alias = alias;
             notes.push(note);
         });
@@ -189,7 +195,7 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes
                 let note_list = $("#" + note_list_id);
                 let html = "";
                 notes_display.forEach(function(disp) {
-                   html += li("note_" + disp[1], disp[0]
+                   html += li(note_prefix + "_" + disp[1], disp[0]
                         + "<span class=\"badge badge-dark badge-pill\">" + disp[3] + "</span>");
                 });
 
@@ -204,7 +210,7 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes
                     });
 
                     // When an emitter is clicked, it is removed
-                    line_obj.click(removeNote(disp, note_prefix, notes_display, user_note_field, profile_pic_field));
+                    line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
                 });
             });
         });
diff --git a/templates/base.html b/templates/base.html
index 2f2f4ab4..6af81fe8 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -117,6 +117,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
     </nav>
     <div class="container-fluid my-3" style="max-width: 1600px;">
         {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
+        <div id="messages"></div>
         {% block content %}
             <p>Default content...</p>
         {% endblock content %}
diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html
index 5cfccb37..c5dcef00 100644
--- a/templates/note/conso_form.html
+++ b/templates/note/conso_form.html
@@ -130,7 +130,7 @@
 {% block extrajavascript %}
     <script type="text/javascript" src="/static/js/consos.js"></script>
     <script type="text/javascript">
-        let CSRF_TOKEN = "{{ csrf_token }}";
+        var CSRF_TOKEN = "{{ csrf_token }}";
 
         {% for button in transaction_templates %}
             {% if button.display %}
diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html
index ff8504bc..842d8152 100644
--- a/templates/note/transaction_form.html
+++ b/templates/note/transaction_form.html
@@ -6,32 +6,133 @@ SPDX-License-Identifier: GPL-2.0-or-later
 {% load i18n static %}
 
 {% block content %}
-    <form method="post" onsubmit="window.onbeforeunload=null">{% csrf_token %}
-        {% if form.non_field_errors %}
-            <p class="errornote">
-                {% for error in form.non_field_errors %}
-                    {{ error }}
-                {% endfor %}
-            </p>
-        {% endif %}
-        <fieldset class="module aligned">
-            {% for field in form %}
-                <div class="form-row{% if field.errors %} errors{% endif %}">
-                    {{ field.errors }}
-                    <div>
-                        {{ field.label_tag }}
-                        {% if field.is_readonly %}
-                            <div class="readonly">{{ field.contents }}</div>
-                        {% else %}
-                            {{ field }}
-                        {% endif %}
-                        {% if field.field.help_text %}
-                            <div class="help">{{ field.field.help_text|safe }}</div>
-                        {% endif %}
-                    </div>
+    <div class="row">
+        <div class="col-md-6">
+            <div class="card border-success shadow mb-4">
+                <div class="card-header">
+                    <p class="card-text font-weight-bold">
+                        Sélection des émetteurs
+                    </p>
                 </div>
-            {% endfor %}
-        </fieldset>
-        <input type="submit" value="{% trans 'Transfer' %}">
-    </form>
+                <ul class="list-group list-group-flush" id="source_note_list">
+                </ul>
+                <div class="card-body">
+                    <input class="form-control mx-auto d-block" type="text" id="source_note" />
+                    <ul class="list-group list-group-flush" id="source_alias_matched">
+                    </ul>
+                </div>
+            </div>
+        </div>
+
+        <div class="col-md-6">
+            <div class="card border-info shadow mb-4">
+                <div class="card-header">
+                    <p class="card-text font-weight-bold">
+                        Sélection des destinataires
+                    </p>
+                </div>
+                <ul class="list-group list-group-flush" id="dest_note_list">
+                </ul>
+                <div class="card-body">
+                    <input class="form-control mx-auto d-block" type="text" id="dest_note" />
+                    <ul class="list-group list-group-flush" id="dest_alias_matched">
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+
+
+    <div class="form-row">
+        <div class="form-group col-md-6">
+            <label for="amount">{% trans "Amount" %} :</label>
+            <input class="form-control mx-auto d-block" type="number" min="-20" id="amount" />
+        </div>
+
+        <div class="form-group col-md-6">
+            <label for="reason">{% trans "Reason" %} :</label>
+            <input class="form-control mx-auto d-block" type="text" id="reason" />
+        </div>
+    </div>
+
+    <div class="form-row">
+        <div class="col-md-12">
+            <button id="transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button>
+        </div>
+    </div>
+{% endblock %}
+
+{% block extrajavascript %}
+    <script>
+        var CSRF_TOKEN = "{{ csrf_token }}";
+
+        var sources = [];
+        var sources_notes_display = [];
+        var dests = [];
+        var dests_notes_display = [];
+
+        $(document).ready(function() {
+           autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display,
+                "source_alias", "source_note");
+           autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display,
+                "dest_alias", "dest_note");
+        });
+
+        $("#transfer").click(function() {
+            sources_notes_display.forEach(function(source) {
+                dests_notes_display.forEach(function(dest) {
+                    $.post("/api/note/transaction/transaction/",
+                    {
+                        "csrfmiddlewaretoken": CSRF_TOKEN,
+                        "quantity": source[3] * dest[3],
+                        "amount": $("#amount").val(),
+                        "reason": $("#reason").val() + " (Transfert)",
+                        "valid": true,
+                        "polymorphic_ctype": {{ polymorphic_ctype }},
+                        "resourcetype": "Transaction",
+                        "source": source[1],
+                        "destination": dest[1]
+                    }, function() {
+                        sources_notes_display.length = 0;
+                        sources.length = 0;
+                        dests_notes_display.length = 0;
+                        dests.length = 0;
+                        $("#source_note_list").html("");
+                        $("#dest_note_list").html("");
+                        $("#source_alias_matched").html("");
+                        $("#dest_alias_matched").html("");
+                        $("#amount").val("");
+                        $("#reason").val("");
+                        refreshBalance();
+
+                        let msgDiv = $("#messages");
+                        let html = msgDiv.html();
+                        html += "<div class=\"alert alert-success\">Le transfert de "
+                            + pretty_money(source[3] * dest[3] * $("#amount").val()) + " de la note " + source[0]
+                            + " vers la note " + dest[0] + " a été fait avec succès !</div>\n";
+                        msgDiv.html(html);
+                    }).fail(function (err) {
+                        sources_notes_display.length = 0;
+                        sources.length = 0;
+                        dests_notes_display.length = 0;
+                        dests.length = 0;
+                        $("#source_note_list").html("");
+                        $("#dest_note_list").html("");
+                        $("#source_alias_matched").html("");
+                        $("#dest_alias_matched").html("");
+                        $("#amount").val("");
+                        $("#reason").val("");
+                        refreshBalance();
+
+                        let msgDiv = $("#messages");
+                        let html = msgDiv.html();
+                        html += "<div class=\"alert alert-danger\">Le transfert de "
+                            + pretty_money(source[3] * dest[3] * $("#amount").val()) + " de la note " + source[0]
+                            + " vers la note " + dest[0] + " a échoué : " + err.responseText + "</div>\n";
+                        msgDiv.html(html);
+                    });
+                });
+            });
+        });
+    </script>
 {% endblock %}
-- 
GitLab