mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Better transfers
This commit is contained in:
		| @@ -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, | ||||
|                     }, | ||||
|                 ), | ||||
|         } | ||||
|   | ||||
| @@ -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): | ||||
|     """ | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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)); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
| @@ -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 %} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user