mirror of https://gitlab.crans.org/bde/nk20
Better transfers
This commit is contained in:
parent
5782e91aa8
commit
7b69ffdefe
|
@ -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.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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in New Issue