Better transfers

This commit is contained in:
Yohann D'ANELLO 2020-03-12 23:12:49 +01:00
parent 5782e91aa8
commit 7b69ffdefe
7 changed files with 158 additions and 110 deletions

View File

@ -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,
},
),
}

View File

@ -5,24 +5,22 @@ from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ 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 django_tables2 import SingleTableView
from .forms import TransactionForm, TransactionTemplateForm from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction
from .tables import HistoryTable from .tables import HistoryTable
class TransactionCreate(LoginRequiredMixin, CreateView): class TransactionCreate(LoginRequiredMixin, TemplateView):
""" """
Show transfer page Show transfer page
TODO: If user have sufficient rights, they can transfer from an other note TODO: If user have sufficient rights, they can transfer from an other note
""" """
model = Transaction template_name = "note/transaction_form.html"
form_class = TransactionForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """
@ -31,26 +29,10 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _('Transfer money from your account ' context['title'] = _('Transfer money from your account '
'to one or others') 'to one or others')
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
context['no_cache'] = True
return context 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): class NoteAutocomplete(autocomplete.Select2QuerySetView):
""" """

View File

@ -408,6 +408,10 @@ msgstr "Un modèle de transaction avec un nom similaire existe déjà."
msgid "amount" msgid "amount"
msgstr "montant" msgstr "montant"
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
msgid "Amount"
msgstr "Montant"
#: apps/note/models/transactions.py:57 #: apps/note/models/transactions.py:57
msgid "in centimes" msgid "in centimes"
msgstr "en centimes" msgstr "en centimes"
@ -428,6 +432,9 @@ msgstr "quantité"
msgid "reason" msgid "reason"
msgstr "raison" msgstr "raison"
msgid "Reason"
msgstr "Raison"
#: apps/note/models/transactions.py:115 #: apps/note/models/transactions.py:115
msgid "valid" msgid "valid"
msgstr "valide" msgstr "valide"

View File

@ -26,7 +26,8 @@ function pretty_money(value) {
if (value % 100 === 0) if (value % 100 === 0)
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €"; return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €";
else 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) { aliases.results.forEach(function(alias) {
getJSONSync("/api/note/note/" + alias.note + "/?format=json", function (note) { getJSONSync("/api/note/note/" + alias.note + "/?format=json", function (note) {
fun(note, alias); 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 d The note to remove
* @param note_prefix The prefix of the identifiers of the <li> blocks of the emitters * @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 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 * @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) * consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note * @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) * (useful in consumptions, put null if not used)
* @returns an anonymous function to be compatible with jQuery events * @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() { return (function() {
let new_notes_display = []; let new_notes_display = [];
let html = ""; 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>"); + "<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) { notes_display.forEach(function (disp) {
obj = $("#" + note_prefix + "_" + disp[1]); let obj = $("#" + note_prefix + "_" + disp[1]);
obj.click(removeNote(disp, note_prefix, notes_display, user_note_field, profile_pic_field)); obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
obj.hover(function() { obj.hover(function() {
displayNote(disp[2], disp[0], user_note_field, profile_pic_field); 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 = ""; let aliases_matched_html = "";
// Get matched notes with the given pattern // Get matched notes with the given pattern
getMatchedNotes(pattern, function(note, alias) { 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; note.alias = alias;
notes.push(note); 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 note_list = $("#" + note_list_id);
let html = ""; let html = "";
notes_display.forEach(function(disp) { 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>"); + "<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 // 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));
}); });
}); });
}); });

View File

@ -117,6 +117,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</nav> </nav>
<div class="container-fluid my-3" style="max-width: 1600px;"> <div class="container-fluid my-3" style="max-width: 1600px;">
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %} {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
<div id="messages"></div>
{% block content %} {% block content %}
<p>Default content...</p> <p>Default content...</p>
{% endblock content %} {% endblock content %}

View File

@ -130,7 +130,7 @@
{% block extrajavascript %} {% block extrajavascript %}
<script type="text/javascript" src="/static/js/consos.js"></script> <script type="text/javascript" src="/static/js/consos.js"></script>
<script type="text/javascript"> <script type="text/javascript">
let CSRF_TOKEN = "{{ csrf_token }}"; var CSRF_TOKEN = "{{ csrf_token }}";
{% for button in transaction_templates %} {% for button in transaction_templates %}
{% if button.display %} {% if button.display %}

View File

@ -6,32 +6,133 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% load i18n static %} {% load i18n static %}
{% block content %} {% block content %}
<form method="post" onsubmit="window.onbeforeunload=null">{% csrf_token %} <div class="row">
{% if form.non_field_errors %} <div class="col-md-6">
<p class="errornote"> <div class="card border-success shadow mb-4">
{% for error in form.non_field_errors %} <div class="card-header">
{{ error }} <p class="card-text font-weight-bold">
{% endfor %} Sélection des émetteurs
</p> </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> </div>
{% endfor %} <ul class="list-group list-group-flush" id="source_note_list">
</fieldset> </ul>
<input type="submit" value="{% trans 'Transfer' %}"> <div class="card-body">
</form> <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 %} {% endblock %}