Ajout faire des transferts

This commit is contained in:
Aurélien Delobelle 2016-08-26 15:30:40 +02:00
parent 9b548c9e45
commit 27b0e3737d
9 changed files with 445 additions and 103 deletions

View file

@ -3,10 +3,12 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.forms import modelformset_factory from django.forms import modelformset_factory, inlineformset_factory
from django.forms.models import BaseInlineFormSet
from django.utils import timezone from django.utils import timezone
from kfet.models import (Account, Checkout, Article, OperationGroup, Operation, from kfet.models import (Account, Checkout, Article, OperationGroup, Operation,
CheckoutStatement, ArticleCategory, Settings, AccountNegative) CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer,
TransferGroup)
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
# ----- # -----
@ -346,3 +348,39 @@ class SettingsForm(forms.ModelForm):
class FilterHistoryForm(forms.Form): class FilterHistoryForm(forms.Form):
checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all()) checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all())
accounts = forms.ModelMultipleChoiceField(queryset = Account.objects.all()) accounts = forms.ModelMultipleChoiceField(queryset = Account.objects.all())
# -----
# Transfer forms
# -----
class TransferGroupForm(forms.ModelForm):
class Meta:
model = TransferGroup
fields = ['comment']
class TransferForm(forms.ModelForm):
from_acc = forms.ModelChoiceField(
queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13']),
widget = forms.HiddenInput()
)
to_acc = forms.ModelChoiceField(
queryset = Account.objects.exclude(trigramme__in=['LIQ', '#13']),
widget = forms.HiddenInput()
)
def clean_amount(self):
amount = self.cleaned_data['amount']
if amount <= 0:
raise forms.ValidationError("Montant invalide")
return amount
class Meta:
model = Transfer
fields = ['from_acc', 'to_acc', 'amount']
TransferFormSet = modelformset_factory(
Transfer,
form = TransferForm,
min_num = 1, validate_min = True,
extra = 9,
)

View file

@ -101,11 +101,8 @@ class Account(models.Model):
data['is_free'] = True data['is_free'] = True
return data return data
def perms_to_perform_operation(self, amount, overdraft_duration_max=None, \ def perms_to_perform_operation(self, amount):
overdraft_amount_max=None):
if overdraft_duration_max is None:
overdraft_duration_max = Settings.OVERDRAFT_DURATION() overdraft_duration_max = Settings.OVERDRAFT_DURATION()
if overdraft_amount_max is None:
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT() overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
perms = set() perms = set()
stop_ope = False stop_ope = False
@ -572,17 +569,27 @@ class Settings(models.Model):
@staticmethod @staticmethod
def OVERDRAFT_DURATION(): def OVERDRAFT_DURATION():
overdraft_duration = cache.get('OVERDRAFT_DURATION')
if overdraft_duration:
return overdraft_duration
try: try:
return Settings.setting_inst("OVERDRAFT_DURATION").value_duration overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration
except Settings.DoesNotExist: except Settings.DoesNotExist:
return timedelta() overdraft_duration = timedelta()
cache.set('OVERDRAFT_DURATION', overdraft_duration)
return overdraft_duration
@staticmethod @staticmethod
def OVERDRAFT_AMOUNT(): def OVERDRAFT_AMOUNT():
overdraft_amount = cache.get('OVERDRAFT_AMOUNT')
if overdraft_amount:
return overdraft_amount
try: try:
return Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
except Settings.DoesNotExist: except Settings.DoesNotExist:
return 0 overdraft_amount = 0
cache.set('OVERDRAFT_AMOUNT', overdraft_amount)
return overdraft_amount
def CANCEL_DURATION(): def CANCEL_DURATION():
try: try:
@ -614,5 +621,11 @@ class Settings(models.Model):
s.value_duration = timedelta(minutes=5) # 5min s.value_duration = timedelta(minutes=5) # 5min
s.save() s.save()
@staticmethod
def empty_cache():
cache.delete_many([
'SUBVENTION_COF','OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT',
])
class GenericTeamToken(models.Model): class GenericTeamToken(models.Model):
token = models.CharField(max_length = 50, unique = True) token = models.CharField(max_length = 50, unique = True)

View file

@ -0,0 +1,58 @@
.transfer_formset {
background:#FFF;
}
.transfer_formset thead {
height:40px;
background:#c8102e;
color:#fff;
font-size:20px;
font-weight:bold;
text-align:center;
}
.transfer_form {
height:50px;
}
.transfer_form td {
padding:0 !important;
}
.transfer_form input {
border:0;
border-radius:0;
width:100%;
height:100%;
font-family:'Roboto Mono';
font-size:25px;
font-weight:bold;
text-align:center;
text-transform:uppercase;
}
.transfer_form .from_acc_data, .transfer_form .to_acc_data {
width:30%;
text-align:center;
vertical-align:middle;
font-size:20px;
}
.transfer_form .from_acc, .transfer_form .to_acc {
width:15%;
}
.transfer_form .from_acc {
border-left:1px solid #ddd;
}
.transfer_form .to_acc {
border-right:1px solid #ddd;
}
.transfer_form .amount {
width:10%;
}

View file

@ -9,6 +9,7 @@ $(document).ready(function() {
} }
}); });
if (typeof Cookies !== 'undefined') {
// Retrieving csrf token // Retrieving csrf token
csrftoken = Cookies.get('csrftoken'); csrftoken = Cookies.get('csrftoken');
// Appending csrf token to ajax post requests // Appending csrf token to ajax post requests
@ -23,7 +24,7 @@ $(document).ready(function() {
} }
} }
}); });
}
}); });
function dateUTCToParis(date) { function dateUTCToParis(date) {
@ -40,3 +41,68 @@ function amountToUKF(amount, is_cof=false) {
var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1; var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1;
return Math.round(amount * coef_cof * 10); return Math.round(amount * coef_cof * 10);
} }
function isValidTrigramme(trigramme) {
var pattern = /^[^a-z]{3}$/;
return trigramme.match(pattern);
}
function getErrorsHtml(data) {
var content = '';
if ('operation_group' in data['errors']) {
content += 'Général';
content += '<ul>';
if (data['errors']['operation_group'].indexOf('on_acc') != -1)
content += '<li>Pas de compte sélectionné</li>';
if (data['errors']['operation_group'].indexOf('checkout') != -1)
content += '<li>Pas de caisse sélectionnée</li>';
content += '</ul>';
}
if ('missing_perms' in data['errors']) {
content += 'Permissions manquantes';
content += '<ul>';
for (var i=0; i<data['errors']['missing_perms'].length; i++)
content += '<li>'+data['errors']['missing_perms'][i]+'</li>';
content += '</ul>';
}
if ('negative' in data['errors'])
content += '<a class="btn btn-primary" href="/k-fet/accounts/'+account_data['trigramme']+'/edit" target="_blank">Autorisation de négatif requise</a>';
if ('addcost' in data['errors']) {
content += '<ul>';
if (data['errors']['addcost'].indexOf('__all__') != -1)
content += '<li>Compte invalide</li>';
if (data['errors']['addcost'].indexOf('amount') != -1)
content += '<li>Montant invalide</li>';
content += '</ul>';
}
return content;
}
function requestAuth(data, callback, focus_next = null) {
var content = getErrorsHtml(data);
content += '<input type="password" name="password" autofocus>',
$.confirm({
title: 'Authentification requise',
content: content,
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirm: function() {
var password = this.$content.find('input').val();
callback(password);
},
onOpen: function() {
var that = this;
this.$content.find('input').on('keypress', function(e) {
if (e.keyCode == 13)
that.$confirmButton.click();
});
},
onClose: function() {
if (focus_next)
this._lastFocused = focus_next;
}
});
}

View file

@ -141,9 +141,8 @@ $(document).ready(function() {
// Initializing // Initializing
var account_container = $('#account'); var account_container = $('#account');
var triInput = $('#id_trigramme') var triInput = $('#id_trigramme');
var triPattern = /^[^a-z]{3}$/ var account_data = {};
var account_data = {}
var account_data_default = { var account_data_default = {
'id' : 0, 'id' : 0,
'name' : '', 'name' : '',
@ -155,7 +154,7 @@ $(document).ready(function() {
'is_frozen' : false, 'is_frozen' : false,
'departement': '', 'departement': '',
'nickname' : '', 'nickname' : '',
} };
// Display data // Display data
function displayAccountData() { function displayAccountData() {
@ -212,7 +211,7 @@ $(document).ready(function() {
function retrieveAccountData(tri) { function retrieveAccountData(tri) {
$.ajax({ $.ajax({
dataType: "json", dataType: "json",
url : "{% url 'kfet.kpsul.account_data' %}", url : "{% url 'kfet.account.read.json' %}",
method : "POST", method : "POST",
data : { trigramme: tri }, data : { trigramme: tri },
}) })
@ -229,7 +228,7 @@ $(document).ready(function() {
triInput.on('input', function() { triInput.on('input', function() {
var tri = triInput.val().toUpperCase(); var tri = triInput.val().toUpperCase();
// Checking if tri is valid to avoid sending requests // Checking if tri is valid to avoid sending requests
if (tri.match(triPattern)) { if (isValidTrigramme(tri)) {
retrieveAccountData(tri); retrieveAccountData(tri);
} else { } else {
resetAccountData(); resetAccountData();
@ -318,31 +317,6 @@ $(document).ready(function() {
// Auth // Auth
// ----- // -----
function requestAuth(data, callback) {
var content = getErrorsHtml(data);
content += '<input type="password" name="password" autofocus>',
$.confirm({
title: 'Authentification requise',
content: content,
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirm: function() {
var password = this.$content.find('input').val();
callback(password);
},
onOpen: function() {
var that = this;
this.$content.find('input').on('keypress', function(e) {
if (e.keyCode == 13)
that.$confirmButton.click();
});
},
onClose: function() { this._lastFocused = articleSelect; }
});
}
function askComment(callback) { function askComment(callback) {
var comment = $('#id_comment').val(); var comment = $('#id_comment').val();
$.confirm({ $.confirm({
@ -374,38 +348,7 @@ $(document).ready(function() {
// ----- // -----
// Errors ajax // Errors ajax
// ----- // -----
x
function getErrorsHtml(data) {
var content = '';
if ('operation_group' in data['errors']) {
content += 'Général';
content += '<ul>';
if (data['errors']['operation_group'].indexOf('on_acc') != -1)
content += '<li>Pas de compte sélectionné</li>';
if (data['errors']['operation_group'].indexOf('checkout') != -1)
content += '<li>Pas de caisse sélectionnée</li>';
content += '</ul>';
}
if ('missing_perms' in data['errors']) {
content += 'Permissions manquantes';
content += '<ul>';
for (var i=0; i<data['errors']['missing_perms'].length; i++)
content += '<li>'+data['errors']['missing_perms'][i]+'</li>';
content += '</ul>';
}
if ('negative' in data['errors'])
content += '<a class="btn btn-primary" href="/k-fet/accounts/'+account_data['trigramme']+'/edit" target="_blank">Autorisation de négatif requise</a>';
if ('addcost' in data['errors']) {
content += '<ul>';
if (data['errors']['addcost'].indexOf('__all__') != -1)
content += '<li>Compte invalide</li>';
if (data['errors']['addcost'].indexOf('amount') != -1)
content += '<li>Montant invalide</li>';
content += '</ul>';
}
return content;
}
function displayErrors(html) { function displayErrors(html) {
$.alert({ $.alert({
title: 'Erreurs', title: 'Erreurs',
@ -445,7 +388,7 @@ $(document).ready(function() {
var data = $xhr.responseJSON; var data = $xhr.responseJSON;
switch ($xhr.status) { switch ($xhr.status) {
case 403: case 403:
requestAuth(data, performOperations); requestAuth(data, performOperations, articleSelect);
break; break;
case 400: case 400:
if ('need_comment' in data['errors']) { if ('need_comment' in data['errors']) {
@ -493,7 +436,7 @@ $(document).ready(function() {
case 403: case 403:
requestAuth(data, function(password) { requestAuth(data, function(password) {
cancelOperations(opes_array, password); cancelOperations(opes_array, password);
}); }, triInput);
break; break;
case 400: case 400:
displayErrors(getErrorsHtml(data)); displayErrors(getErrorsHtml(data));
@ -1023,7 +966,7 @@ $(document).ready(function() {
case 403: case 403:
requestAuth(data, function(password) { requestAuth(data, function(password) {
sendAddcost(trigramme, amount, password); sendAddcost(trigramme, amount, password);
}); }, triInput);
break; break;
case 400: case 400:
askAddcost(getErrorsHtml(data)); askAddcost(getErrorsHtml(data));

View file

@ -0,0 +1,16 @@
{% extends 'kfet/base.html' %}
{% block title %}Transferts{% endblock %}
{% block content-header-title %}Transferts{% endblock %}
{% block content %}
<div class="row">
<form action="" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Enregistrer">
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,122 @@
{% extends 'kfet/base.html' %}
{% load staticfiles %}
{% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/transfers_form.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
{% endblock %}
{% block title %}Nouveaux transferts{% endblock %}
{% block content-header-title %}Nouveaux transferts{% endblock %}
{% block content %}
{% csrf_token %}
<form id="transfers_form">
<table class="transfer_formset table">
<thead>
<tr>
<td></td>
<td>De</td>
<td><span class="glyphicon glyphicon-euro"></span></td>
<td>Vers</td>
<td></td>
</tr>
</thead>
<tbody>
{% for form in transfer_formset %}
<tr class="transfer_form" id="{{ form.prefix }}">
<td class="from_acc_data"></td>
<td class="from_acc">
<input type="text" name="from_acc" class="input_from_acc" autocomplete="off" spellcheck="false">
{{ form.from_acc }}
</td>
<td class="amount">{{ form.amount }}</td>
<td class="to_acc">
<input type="text" name="to_acc" class="input_to_acc" autocomplete="off" spellcheck="false">
{{ form.to_acc }}
</td>
<td class="to_acc_data"></td>
</tr>
{% endfor %}
</tbody>
</table>
<div>
<label for="comment">Commentaire:</label>
<input type="text" name="comment" id="comment">
{{ transfer_formset.management_form }}
<button type="submit" id="submit" class="btn btn-primary btn-lg">Enregistrer</button>
</div>
</form>
<script type="text/javascript">
$(document).ready(function () {
function getAccountData(trigramme, callback = function() {}) {
$.ajax({
dataType: "json",
url : "{% url 'kfet.account.read.json' %}",
method : "POST",
data : { trigramme: trigramme },
success : callback,
});
}
function updateAccountData(trigramme, $input) {
var $form = $input.closest('.transfer_form');
if ($input.attr('name') == 'from_acc') {
var $data = $form.find('.from_acc_data');
var $next = $form.find('.amount input');
} else {
var $data = $form.find('.to_acc_data');
var $next = $form.next('.transfer_form').find('.from_acc input');
}
var $input_id = $input.next('input');
getAccountData(trigramme, function(data) {
$input_id.val(data.id);
$data.text(data.name);
$next.focus();
});
}
$('.input_from_acc, .input_to_acc').on('input', function() {
var tri = $(this).val().toUpperCase();
if (isValidTrigramme(tri)) {
updateAccountData(tri, $(this));
}
});
$('#transfers_form').on('submit', function(e) {
e.preventDefault();
performTransfers();
});
function performTransfers(password = '') {
var data = $('#transfers_form').serialize();
$.ajax({
dataType: "json",
url : "{% url 'kfet.transfers.perform' %}",
method : "POST",
data : data,
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password != '')
$xhr.setRequestHeader("KFetPassword", password);
},
})
.done(function(data) {
window.location.replace("{% url 'kfet.transfers' %}");
})
.fail(function($xhr) {
var data = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
requestAuth(data, performTransfers);
break;
}
});
}
});
</script>
{% endblock %}

View file

@ -117,8 +117,6 @@ urlpatterns = [
# ----- # -----
url('^k-psul/$', views.kpsul, name = 'kfet.kpsul'), url('^k-psul/$', views.kpsul, name = 'kfet.kpsul'),
url('^k-psul/account_data$', views.kpsul_account_data,
name = 'kfet.kpsul.account_data'),
url('^k-psul/checkout_data$', views.kpsul_checkout_data, url('^k-psul/checkout_data$', views.kpsul_checkout_data,
name = 'kfet.kpsul.checkout_data'), name = 'kfet.kpsul.checkout_data'),
url('^k-psul/perform_operations$', views.kpsul_perform_operations, url('^k-psul/perform_operations$', views.kpsul_perform_operations,
@ -136,17 +134,31 @@ urlpatterns = [
# JSON urls # JSON urls
# ----- # -----
url('^history.json$', views.history_json, url(r'^history.json$', views.history_json,
name = 'kfet.history.json'), name = 'kfet.history.json'),
url(r'^accounts/read.json$', views.account_read_json,
name = 'kfet.account.read.json'),
# ----- # -----
# Settings urls # Settings urls
# ----- # -----
url('^settings/$', url(r'^settings/$',
permission_required('kfet.change_settings')(views.SettingsList.as_view()), permission_required('kfet.change_settings')(views.SettingsList.as_view()),
name = 'kfet.settings'), name = 'kfet.settings'),
url('^settings/(?P<pk>\d+)/edit$', url(r'^settings/(?P<pk>\d+)/edit$',
permission_required('kfet.change_settings')(views.SettingsUpdate.as_view()), permission_required('kfet.change_settings')(views.SettingsUpdate.as_view()),
name = 'kfet.settings.update'), name = 'kfet.settings.update'),
# -----
# Transfers urls
# -----
url(r'^transfers/$', views.home,
name = 'kfet.transfers'),
url(r'^transfers/new$', views.transfer_create,
name = 'kfet.transfers.create'),
url(r'^transfers/perform$', views.perform_transfers,
name = 'kfet.transfers.perform'),
] ]

View file

@ -600,7 +600,7 @@ def kpsul_get_settings(request):
return JsonResponse(data) return JsonResponse(data)
@permission_required('kfet.is_team') @permission_required('kfet.is_team')
def kpsul_account_data(request): def account_read_json(request):
trigramme = request.POST.get('trigramme', '') trigramme = request.POST.get('trigramme', '')
account = get_object_or_404(Account, trigramme=trigramme) account = get_object_or_404(Account, trigramme=trigramme)
data = { 'id': account.pk, 'name': account.name, 'email': account.email, data = { 'id': account.pk, 'name': account.name, 'email': account.email,
@ -931,13 +931,9 @@ def kpsul_cancel_operations(request):
return JsonResponse(data) return JsonResponse(data)
# Checking permissions or stop # Checking permissions or stop
overdraft_duration_max = Settings.OVERDRAFT_DURATION()
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
for account in to_accounts_balances: for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation( (perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account], amount = to_accounts_balances[account])
overdraft_duration_max = overdraft_duration_max,
overdraft_amount_max = overdraft_amount_max)
required_perms |= perms required_perms |= perms
stop_all = stop_all or stop stop_all = stop_all or stop
@ -1125,5 +1121,83 @@ class SettingsUpdate(SuccessMessageMixin, UpdateView):
form.add_error(None, 'Permission refusée') form.add_error(None, 'Permission refusée')
return self.form_invalid(form) return self.form_invalid(form)
# Creating # Creating
Settings.empty_cache()
return super(SettingsUpdate, self).form_valid(form) return super(SettingsUpdate, self).form_valid(form)
# -----
# Transfer views
# -----
def transfer_create(request):
transfer_formset = TransferFormSet(queryset=Transfer.objects.none())
return render(request, 'kfet/transfers_create.html',
{ 'transfer_formset': transfer_formset })
def perform_transfers(request):
data = { 'errors': {}, 'transfers': [], 'transfergroup': 0 }
# Checking transfer_formset
transfer_formset = TransferFormSet(request.POST)
if not transfer_formset.is_valid():
return JsonResponse({ 'errors': list(transfer_formset.errors)}, status=400)
transfers = transfer_formset.save(commit = False)
# Initializing vars
required_perms = set() # Required perms to perform all transfers
to_accounts_balances = defaultdict(lambda:0) # For balances of accounts
for transfer in transfers:
to_accounts_balances[transfer.from_acc] -= transfer.amount
to_accounts_balances[transfer.to_acc] += transfer.amount
stop_all = False
# Checking if ok on all accounts
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
data['errors']['missing_perms'] = missing_perms
if stop_all:
data['errors']['negative'] = True
return JsonResponse(data, status=403)
with transaction.atomic():
# Updating balances accounts
for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update(
balance = F('balance') + to_accounts_balances[account])
account.refresh_from_db()
if account.balance < 0:
if hasattr(account, 'negative'):
if not account.negative.start:
account.negative.start = timezone.now()
account.negative.save()
else:
negative = AccountNegative(
account = account, start = timezone.now())
negative.save()
elif (hasattr(account, 'negative')
and not account.negative.balance_offset):
account.negative.delete()
# Creating transfer group
transfergroup = TransferGroup()
if required_perms:
transfergroup.valid_by = request.user.profile.account_kfet
transfergroup.save()
data['transfergroup'] = transfergroup.pk
# Saving all transfers with group
for transfer in transfers:
transfer.group = transfergroup
transfer.save()
data['transfers'].append(transfer.pk)
return JsonResponse(data)