diff --git a/kfet/forms.py b/kfet/forms.py
index 8302067a..6709beb1 100644
--- a/kfet/forms.py
+++ b/kfet/forms.py
@@ -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,
+)
diff --git a/kfet/models.py b/kfet/models.py
index a2d50cdc..51560d4b 100644
--- a/kfet/models.py
+++ b/kfet/models.py
@@ -101,12 +101,9 @@ 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:
- overdraft_duration_max = Settings.OVERDRAFT_DURATION()
- if overdraft_amount_max is None:
- overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
+ def perms_to_perform_operation(self, amount):
+ overdraft_duration_max = Settings.OVERDRAFT_DURATION()
+ overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
perms = set()
stop_ope = False
# Checking is cash account
@@ -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)
diff --git a/kfet/static/kfet/css/transfers_form.css b/kfet/static/kfet/css/transfers_form.css
new file mode 100644
index 00000000..f222e490
--- /dev/null
+++ b/kfet/static/kfet/css/transfers_form.css
@@ -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%;
+}
diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js
index 719fb3d8..1bdeef8e 100644
--- a/kfet/static/kfet/js/kfet.js
+++ b/kfet/static/kfet/js/kfet.js
@@ -9,21 +9,22 @@ $(document).ready(function() {
}
});
- // Retrieving csrf token
- csrftoken = Cookies.get('csrftoken');
- // Appending csrf token to ajax post requests
- function csrfSafeMethod(method) {
- // these HTTP methods do not require CSRF protection
- return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
- }
- $.ajaxSetup({
- beforeSend: function(xhr, settings) {
- if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
- xhr.setRequestHeader("X-CSRFToken", csrftoken);
- }
+ if (typeof Cookies !== 'undefined') {
+ // Retrieving csrf token
+ csrftoken = Cookies.get('csrftoken');
+ // Appending csrf token to ajax post requests
+ function csrfSafeMethod(method) {
+ // these HTTP methods do not require CSRF protection
+ return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
- });
-
+ $.ajaxSetup({
+ beforeSend: function(xhr, settings) {
+ if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+ xhr.setRequestHeader("X-CSRFToken", csrftoken);
+ }
+ }
+ });
+ }
});
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 += '
';
+ if (data['errors']['operation_group'].indexOf('on_acc') != -1)
+ content += '- Pas de compte sélectionné
';
+ if (data['errors']['operation_group'].indexOf('checkout') != -1)
+ content += '- Pas de caisse sélectionnée
';
+ content += '
';
+ }
+ if ('missing_perms' in data['errors']) {
+ content += 'Permissions manquantes';
+ content += '';
+ for (var i=0; i'+data['errors']['missing_perms'][i]+'';
+ content += '
';
+ }
+ if ('negative' in data['errors'])
+ content += 'Autorisation de négatif requise';
+ if ('addcost' in data['errors']) {
+ content += '';
+ if (data['errors']['addcost'].indexOf('__all__') != -1)
+ content += '- Compte invalide
';
+ if (data['errors']['addcost'].indexOf('amount') != -1)
+ content += '- Montant invalide
';
+ content += '
';
+ }
+ return content;
+}
+
+function requestAuth(data, callback, focus_next = null) {
+ var content = getErrorsHtml(data);
+ content += '',
+ $.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;
+ }
+ });
+}
+
diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html
index 2b64c8d5..eed7fc6d 100644
--- a/kfet/templates/kfet/kpsul.html
+++ b/kfet/templates/kfet/kpsul.html
@@ -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 += '',
- $.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 += '';
- if (data['errors']['operation_group'].indexOf('on_acc') != -1)
- content += '- Pas de compte sélectionné
';
- if (data['errors']['operation_group'].indexOf('checkout') != -1)
- content += '- Pas de caisse sélectionnée
';
- content += '
';
- }
- if ('missing_perms' in data['errors']) {
- content += 'Permissions manquantes';
- content += '';
- for (var i=0; i'+data['errors']['missing_perms'][i]+'';
- content += '
';
- }
- if ('negative' in data['errors'])
- content += 'Autorisation de négatif requise';
- if ('addcost' in data['errors']) {
- content += '';
- if (data['errors']['addcost'].indexOf('__all__') != -1)
- content += '- Compte invalide
';
- if (data['errors']['addcost'].indexOf('amount') != -1)
- content += '- Montant invalide
';
- content += '
';
- }
- 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));
diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html
new file mode 100644
index 00000000..4ed58465
--- /dev/null
+++ b/kfet/templates/kfet/transfers.html
@@ -0,0 +1,16 @@
+{% extends 'kfet/base.html' %}
+
+{% block title %}Transferts{% endblock %}
+{% block content-header-title %}Transferts{% endblock %}
+
+{% block content %}
+
+
+
+
+
+{% endblock %}
diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html
new file mode 100644
index 00000000..a28ab892
--- /dev/null
+++ b/kfet/templates/kfet/transfers_create.html
@@ -0,0 +1,122 @@
+{% extends 'kfet/base.html' %}
+{% load staticfiles %}
+
+{% block extra_head %}
+
+
+{% endblock %}
+
+{% block title %}Nouveaux transferts{% endblock %}
+{% block content-header-title %}Nouveaux transferts{% endblock %}
+
+{% block content %}
+
+{% csrf_token %}
+
+
+
+
+
+{% endblock %}
diff --git a/kfet/urls.py b/kfet/urls.py
index 51e6c785..ef8f731a 100644
--- a/kfet/urls.py
+++ b/kfet/urls.py
@@ -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\d+)/edit$',
+ url(r'^settings/(?P\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'),
]
diff --git a/kfet/views.py b/kfet/views.py
index 1596fc44..65199277 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -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)