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.contrib.auth.models import User, Group, Permission
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 kfet.models import (Account, Checkout, Article, OperationGroup, Operation,
CheckoutStatement, ArticleCategory, Settings, AccountNegative)
CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer,
TransferGroup)
from gestioncof.models import CofProfile
# -----
@ -346,3 +348,39 @@ class SettingsForm(forms.ModelForm):
class FilterHistoryForm(forms.Form):
checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.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
return data
def perms_to_perform_operation(self, amount, overdraft_duration_max=None, \
overdraft_amount_max=None):
if overdraft_duration_max is None:
def perms_to_perform_operation(self, amount):
overdraft_duration_max = Settings.OVERDRAFT_DURATION()
if overdraft_amount_max is None:
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
perms = set()
stop_ope = False
@ -572,17 +569,27 @@ class Settings(models.Model):
@staticmethod
def OVERDRAFT_DURATION():
overdraft_duration = cache.get('OVERDRAFT_DURATION')
if overdraft_duration:
return overdraft_duration
try:
return Settings.setting_inst("OVERDRAFT_DURATION").value_duration
overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration
except Settings.DoesNotExist:
return timedelta()
overdraft_duration = timedelta()
cache.set('OVERDRAFT_DURATION', overdraft_duration)
return overdraft_duration
@staticmethod
def OVERDRAFT_AMOUNT():
overdraft_amount = cache.get('OVERDRAFT_AMOUNT')
if overdraft_amount:
return overdraft_amount
try:
return Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
except Settings.DoesNotExist:
return 0
overdraft_amount = 0
cache.set('OVERDRAFT_AMOUNT', overdraft_amount)
return overdraft_amount
def CANCEL_DURATION():
try:
@ -614,5 +621,11 @@ class Settings(models.Model):
s.value_duration = timedelta(minutes=5) # 5min
s.save()
@staticmethod
def empty_cache():
cache.delete_many([
'SUBVENTION_COF','OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT',
])
class GenericTeamToken(models.Model):
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
csrftoken = Cookies.get('csrftoken');
// Appending csrf token to ajax post requests
@ -23,7 +24,7 @@ $(document).ready(function() {
}
}
});
}
});
function dateUTCToParis(date) {
@ -40,3 +41,68 @@ function amountToUKF(amount, is_cof=false) {
var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1;
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
var account_container = $('#account');
var triInput = $('#id_trigramme')
var triPattern = /^[^a-z]{3}$/
var account_data = {}
var triInput = $('#id_trigramme');
var account_data = {};
var account_data_default = {
'id' : 0,
'name' : '',
@ -155,7 +154,7 @@ $(document).ready(function() {
'is_frozen' : false,
'departement': '',
'nickname' : '',
}
};
// Display data
function displayAccountData() {
@ -212,7 +211,7 @@ $(document).ready(function() {
function retrieveAccountData(tri) {
$.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.account_data' %}",
url : "{% url 'kfet.account.read.json' %}",
method : "POST",
data : { trigramme: tri },
})
@ -229,7 +228,7 @@ $(document).ready(function() {
triInput.on('input', function() {
var tri = triInput.val().toUpperCase();
// Checking if tri is valid to avoid sending requests
if (tri.match(triPattern)) {
if (isValidTrigramme(tri)) {
retrieveAccountData(tri);
} else {
resetAccountData();
@ -318,31 +317,6 @@ $(document).ready(function() {
// 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) {
var comment = $('#id_comment').val();
$.confirm({
@ -374,38 +348,7 @@ $(document).ready(function() {
// -----
// Errors ajax
// -----
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;
}
x
function displayErrors(html) {
$.alert({
title: 'Erreurs',
@ -445,7 +388,7 @@ $(document).ready(function() {
var data = $xhr.responseJSON;
switch ($xhr.status) {
case 403:
requestAuth(data, performOperations);
requestAuth(data, performOperations, articleSelect);
break;
case 400:
if ('need_comment' in data['errors']) {
@ -493,7 +436,7 @@ $(document).ready(function() {
case 403:
requestAuth(data, function(password) {
cancelOperations(opes_array, password);
});
}, triInput);
break;
case 400:
displayErrors(getErrorsHtml(data));
@ -1023,7 +966,7 @@ $(document).ready(function() {
case 403:
requestAuth(data, function(password) {
sendAddcost(trigramme, amount, password);
});
}, triInput);
break;
case 400:
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/account_data$', views.kpsul_account_data,
name = 'kfet.kpsul.account_data'),
url('^k-psul/checkout_data$', views.kpsul_checkout_data,
name = 'kfet.kpsul.checkout_data'),
url('^k-psul/perform_operations$', views.kpsul_perform_operations,
@ -136,17 +134,31 @@ urlpatterns = [
# JSON urls
# -----
url('^history.json$', views.history_json,
url(r'^history.json$', views.history_json,
name = 'kfet.history.json'),
url(r'^accounts/read.json$', views.account_read_json,
name = 'kfet.account.read.json'),
# -----
# Settings urls
# -----
url('^settings/$',
url(r'^settings/$',
permission_required('kfet.change_settings')(views.SettingsList.as_view()),
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()),
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)
@permission_required('kfet.is_team')
def kpsul_account_data(request):
def account_read_json(request):
trigramme = request.POST.get('trigramme', '')
account = get_object_or_404(Account, trigramme=trigramme)
data = { 'id': account.pk, 'name': account.name, 'email': account.email,
@ -931,13 +931,9 @@ def kpsul_cancel_operations(request):
return JsonResponse(data)
# Checking permissions or stop
overdraft_duration_max = Settings.OVERDRAFT_DURATION()
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
for account in to_accounts_balances:
(perms, stop) = account.perms_to_perform_operation(
amount = to_accounts_balances[account],
overdraft_duration_max = overdraft_duration_max,
overdraft_amount_max = overdraft_amount_max)
amount = to_accounts_balances[account])
required_perms |= perms
stop_all = stop_all or stop
@ -1125,5 +1121,83 @@ class SettingsUpdate(SuccessMessageMixin, UpdateView):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Creating
Settings.empty_cache()
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)