Merge branch 'master' into Aufinal/inventaires

This commit is contained in:
Ludovic Stephan 2017-04-03 16:01:51 -03:00
commit f46ba0dd6c
27 changed files with 746 additions and 245 deletions

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bda', '0010_spectaclerevente_shotgun'),
]
operations = [
migrations.AddField(
model_name='tirage',
name='appear_catalogue',
field=models.BooleanField(
default=False,
verbose_name='Tirage à afficher dans le catalogue'
),
),
]

View file

@ -18,6 +18,7 @@ class Tirage(models.Model):
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
tokens = models.TextField("Graine(s) du tirage", blank=True)
active = models.BooleanField("Tirage actif", default=False)
appear_catalogue = models.BooleanField("Tirage à afficher dans le catalogue", default=False)
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
default=False)
@ -78,6 +79,15 @@ class Spectacle(models.Model):
self.price
)
def getImgUrl(self):
"""
Cette fonction permet d'obtenir l'URL de l'image, si elle existe
"""
try:
return self.image.url
except:
return None
def send_rappel(self):
"""
Envoie un mail de rappel à toutes les personnes qui ont une place pour
@ -315,10 +325,11 @@ class SpectacleRevente(models.Model):
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
context['acheteur'] = inscrit.user
new_context = dict(context)
new_context['acheteur'] = inscrit.user
datatuple.append((
'bda-revente-loser',
context,
new_context,
settings.MAIL_DATA['revente']['FROM'],
[inscrit.user.email]
))

View file

@ -47,4 +47,6 @@ urlpatterns = [
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'),
url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue,
name='bda-catalogue'),
]

View file

@ -3,11 +3,11 @@
import random
import hashlib
import time
import json
from datetime import timedelta
from custommail.shortcuts import (
send_mass_custom_mail, send_custom_mail, render_custom_mail
)
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
@ -15,18 +15,24 @@ from django.db import models, transaction
from django.core import serializers
from django.db.models import Count, Q, Sum
from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.http import (
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
)
from django.core.urlresolvers import reverse
from django.conf import settings
from django.utils import timezone, formats
from django.views.generic.list import ListView
from django.core.exceptions import ObjectDoesNotExist
from gestioncof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
Tirage, SpectacleRevente
from bda.models import (
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
SpectacleRevente, Salle, Quote, CategorieSpectacle
)
from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
InscriptionReventeForm, SoldForm
from bda.forms import (
BaseBdaFormSet, TokenForm, ResellForm, AnnulForm, InscriptionReventeForm,
SoldForm
)
@cof_required
@ -639,3 +645,98 @@ def descriptions_spectacles(request, tirage_id):
return HttpResponseBadRequest(
"La variable GET 'location' doit contenir un entier")
return render(request, 'descriptions.html', {'shows': shows_qs.all()})
def catalogue(request, request_type):
"""
Vue destinée à communiquer avec un client AJAX, fournissant soit :
- la liste des tirages
- les catégories et salles d'un tirage
- les descriptions d'un tirage (filtrées selon la catégorie et la salle)
"""
if request_type == "list":
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
data_return = list(
Tirage.objects.filter(appear_catalogue=True).values('id', 'title'))
return JsonResponse(data_return, safe=False)
if request_type == "details":
# Dans ce cas on retourne une liste des catégories et des salles
tirage_id = request.GET.get('id', '')
try:
tirage = Tirage.objects.get(id=tirage_id)
except ObjectDoesNotExist:
return HttpResponseBadRequest(
"Aucun tirage correspondant à l'id "
+ tirage_id)
except ValueError:
return HttpResponseBadRequest(
"Mauvais format d'identifiant : "
+ tirage_id)
categories = list(
CategorieSpectacle.objects.filter(
spectacle__in=tirage.spectacle_set.all())
.distinct().values('id', 'name'))
locations = list(
Salle.objects.filter(
spectacle__in=tirage.spectacle_set.all())
.distinct().values('id', 'name'))
data_return = {'categories': categories, 'locations': locations}
return JsonResponse(data_return, safe=False)
if request_type == "descriptions":
# Ici on retourne les descriptions correspondant à la catégorie et
# à la salle spécifiées
tirage_id = request.GET.get('id', '')
categories = request.GET.get('category', '[0]')
locations = request.GET.get('location', '[0]')
try:
category_id = json.loads(categories)
location_id = json.loads(locations)
tirage = Tirage.objects.get(id=tirage_id)
shows_qs = tirage.spectacle_set
if not(0 in category_id):
shows_qs = shows_qs.filter(
category__id__in=category_id)
if not(0 in location_id):
shows_qs = shows_qs.filter(
location__id__in=location_id)
except ObjectDoesNotExist:
return HttpResponseBadRequest(
"Impossible de trouver des résultats correspondant "
"à ces caractéristiques : "
+ "id = " + tirage_id
+ ", catégories = " + categories
+ ", salles = " + locations)
except ValueError: # Contient JSONDecodeError
return HttpResponseBadRequest(
"Impossible de parser les paramètres donnés : "
+ "id = " + request.GET.get('id', '')
+ ", catégories = " + request.GET.get('category', '[0]')
+ ", salles = " + request.GET.get('location', '[0]'))
# On convertit les descriptions à envoyer en une liste facilement
# JSONifiable (il devrait y avoir un moyen plus efficace en
# redéfinissant le serializer de JSON)
data_return = [{
'title': spectacle.title,
'category': str(spectacle.category),
'date': str(formats.date_format(
timezone.localtime(spectacle.date),
"SHORT_DATETIME_FORMAT")),
'location': str(spectacle.location),
'vips': spectacle.vips,
'description': spectacle.description,
'slots_description': spectacle.slots_description,
'quotes': list(Quote.objects.filter(spectacle=spectacle).values(
'author', 'text')),
'image': spectacle.getImgUrl(),
'ext_link': spectacle.ext_link,
'price': spectacle.price,
'slots': spectacle.slots
}
for spectacle in shows_qs.all()
]
return JsonResponse(data_return, safe=False)
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur
return HttpResponseBadRequest()

View file

@ -14,6 +14,10 @@ from gestioncof.decorators import buro_required
class Clipper(object):
def __init__(self, clipper, fullname):
if fullname is None:
fullname = ""
assert isinstance(clipper, str)
assert isinstance(fullname, str)
self.clipper = clipper
self.fullname = fullname
@ -62,17 +66,19 @@ def autocomplete(request):
))
if ldap_query != "(&)":
# If none of the bits were legal, we do not perform the query
entries = None
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
entries = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
Clipper(entry.uid.value, entry.cn.value)
for entry in entries
if entry.uid.value
and entry.uid.value not in usernames
]
# Resulting data

View file

@ -215,7 +215,7 @@ def _traitement_other(request, demande, redo):
proposals = proposals.items()
proposed_for = proposed_for.items()
return render(request,
"gestiocof/traitement_demande_petit_cours_autre_niveau.html",
"gestioncof/traitement_demande_petit_cours_autre_niveau.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,

View file

@ -13,6 +13,10 @@ from kfet.models import Account
class Clipper(object):
def __init__(self, clipper, fullname):
if fullname is None:
fullname = ""
assert isinstance(clipper, str)
assert isinstance(fullname, str)
self.clipper = clipper
self.fullname = fullname
@ -80,17 +84,19 @@ def account_create(request):
))
if ldap_query != "(&)":
# If none of the bits were legal, we do not perform the query
entries = None
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
entries = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
Clipper(entry.uid.value, entry.cn.value)
for entry in entries
if entry.uid.value
and entry.uid.value not in usernames
]
# Resulting data

View file

@ -74,8 +74,11 @@ class AccountRestrictForm(AccountForm):
class AccountPwdForm(forms.Form):
pwd1 = forms.CharField(
label="Mot de passe K-Fêt",
help_text="Le mot de passe doit contenir au moins huit caractères",
widget=forms.PasswordInput)
pwd2 = forms.CharField(
label="Confirmer le mot de passe",
widget=forms.PasswordInput)
def clean(self):
@ -128,6 +131,7 @@ class UserRestrictTeamForm(UserForm):
class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt'),
label='Statut équipe',
required=False)
def clean_groups(self):
@ -235,16 +239,20 @@ class CheckoutStatementUpdateForm(forms.ModelForm):
class ArticleForm(forms.ModelForm):
category_new = forms.CharField(
label="Créer une catégorie",
max_length=45,
required = False)
category = forms.ModelChoiceField(
label="Catégorie",
queryset = ArticleCategory.objects.all(),
required = False)
suppliers = forms.ModelMultipleChoiceField(
label="Fournisseurs",
queryset = Supplier.objects.all(),
required = False)
supplier_new = forms.CharField(
label="Créer un fournisseur",
max_length = 45,
required = False)
@ -318,11 +326,10 @@ class KPsulOperationForm(forms.ModelForm):
widget = forms.HiddenInput())
class Meta:
model = Operation
fields = ['type', 'amount', 'is_checkout', 'article', 'article_nb']
fields = ['type', 'amount', 'article', 'article_nb']
widgets = {
'type': forms.HiddenInput(),
'amount': forms.HiddenInput(),
'is_checkout': forms.HiddenInput(),
'article_nb': forms.HiddenInput(),
}
@ -338,7 +345,6 @@ class KPsulOperationForm(forms.ModelForm):
"Un achat nécessite un article et une quantité")
if article_nb < 1:
raise ValidationError("Impossible d'acheter moins de 1 article")
self.cleaned_data['is_checkout'] = True
elif type_ope and type_ope in [Operation.DEPOSIT, Operation.WITHDRAW]:
if not amount or article or article_nb:
raise ValidationError("Bad request")
@ -479,9 +485,7 @@ class OrderArticleForm(forms.Form):
queryset=Article.objects.all(),
widget=forms.HiddenInput(),
)
quantity_ordered = forms.IntegerField(
required=False,
widget=forms.NumberInput(attrs={'class': 'form-control'}))
quantity_ordered = forms.IntegerField(required=False)
def __init__(self, *args, **kwargs):
super(OrderArticleForm, self).__init__(*args, **kwargs)
@ -508,18 +512,14 @@ class OrderArticleToInventoryForm(forms.Form):
)
price_HT = forms.DecimalField(
max_digits = 7, decimal_places = 4,
required = False,
widget=forms.NumberInput(attrs={'class': 'form-control'}))
required = False)
TVA = forms.DecimalField(
max_digits = 7, decimal_places = 2,
required = False,
widget=forms.NumberInput(attrs={'class': 'form-control'}))
required = False)
rights = forms.DecimalField(
max_digits = 7, decimal_places = 4,
required = False,
widget=forms.NumberInput(attrs={'class': 'form-control'}))
quantity_received = forms.IntegerField(
widget=forms.NumberInput(attrs={'class': 'form-control'}))
required = False)
quantity_received = forms.IntegerField()
def __init__(self, *args, **kwargs):
super(OrderArticleToInventoryForm, self).__init__(*args, **kwargs)

View file

@ -0,0 +1,175 @@
"""
Crée des opérations aléatoires réparties sur une période de temps spécifiée
"""
import random
from datetime import timedelta
from decimal import Decimal
from django.utils import timezone
from django.core.management.base import BaseCommand
from kfet.models import (Account, Article, OperationGroup, Operation,
Checkout, Transfer, TransferGroup)
class Command(BaseCommand):
help = "Crée des opérations réparties uniformément sur une période de temps"
def add_arguments(self, parser):
# Nombre d'opérations à créer
parser.add_argument('opes', type=int,
help='Number of opegroups to create')
# Période sur laquelle créer (depuis num_days avant maintenant)
parser.add_argument('days', type=int,
help='Period in which to create opegroups')
# Optionnel : nombre de transfert à créer (défaut 0)
parser.add_argument('--transfers', type=int, default=0,
help='Number of transfers to create (default 0)')
def handle(self, *args, **options):
self.stdout.write("Génération d'opérations")
# Output log vars
opes_created = 0
purchases = 0
transfers = 0
num_ops = options['opes']
num_transfers = options['transfers']
# Convert to seconds
time = options['days'] * 24 * 3600
checkout = Checkout.objects.first()
articles = Article.objects.all()
accounts = Account.objects.exclude(trigramme='LIQ')
liq_account = Account.objects.get(trigramme='LIQ')
try:
con_account = Account.objects.get(
cofprofile__user__first_name='Assurancetourix'
)
except Account.DoesNotExist:
con_account = random.choice(accounts)
for i in range(num_ops):
# Randomly pick account
if random.random() > 0.25:
account = random.choice(accounts)
else:
account = liq_account
# Randomly pick time
at = timezone.now() - timedelta(
seconds=random.randint(0, time))
# Majoration sur compte 'concert'
if random.random() < 0.2:
addcost = True
addcost_for = con_account
addcost_amount = Decimal('0.5')
else:
addcost = False
# Initialize opegroup amount
amount = Decimal('0')
opegroup = OperationGroup.objects.create(
on_acc=account,
checkout=checkout,
at=at,
is_cof=account.cofprofile.is_cof
)
# Generating operations
ope_list = []
for j in range(random.randint(1, 4)):
# Operation type
typevar = random.random()
# 0.1 probability to have a charge
if typevar > 0.9 and account != liq_account:
ope = Operation(
group=opegroup,
type=Operation.DEPOSIT,
is_checkout=(random.random() > 0.2),
amount=Decimal(random.randint(1, 99)/10)
)
# 0.1 probability to have a withdrawal
elif typevar > 0.8 and account != liq_account:
ope = Operation(
group=opegroup,
type=Operation.WITHDRAW,
is_checkout=(random.random() > 0.2),
amount=-Decimal(random.randint(1, 99)/10)
)
else:
article = random.choice(articles)
nb = random.randint(1, 5)
ope = Operation(
group=opegroup,
type=Operation.PURCHASE,
amount=-article.price*nb,
article=article,
article_nb=nb
)
purchases += 1
if addcost:
ope.addcost_for = addcost_for
ope.addcost_amount = addcost_amount * nb
ope.amount -= ope.addcost_amount
ope_list.append(ope)
amount += ope.amount
Operation.objects.bulk_create(ope_list)
opes_created += len(ope_list)
opegroup.amount = amount
opegroup.save()
# Transfer generation
for i in range(num_transfers):
# Randomly pick time
at = timezone.now() - timedelta(
seconds=random.randint(0, time))
# Choose whether to have a comment
if random.random() > 0.5:
comment = "placeholder comment"
else:
comment = ""
transfergroup = TransferGroup.objects.create(
at=at,
comment=comment,
valid_by=random.choice(accounts)
)
# Randomly generate transfer
transfer_list = []
for i in range(random.randint(1, 4)):
transfer_list.append(Transfer(
group=transfergroup,
from_acc=random.choice(accounts),
to_acc=random.choice(accounts),
amount=Decimal(random.randint(1, 99)/10)
))
Transfer.objects.bulk_create(transfer_list)
transfers += len(transfer_list)
self.stdout.write(
"- {:d} opérations créées dont {:d} commandes d'articles"
.format(opes_created, purchases))
if transfers:
self.stdout.write("- {:d} transferts créés"
.format(transfers))

View file

@ -5,15 +5,15 @@ Crée des utilisateurs, des articles et des opérations aléatoires
import os
import random
from datetime import timedelta
from decimal import Decimal
from django.utils import timezone
from django.contrib.auth.models import User, Group, Permission, ContentType
from django.core.management import call_command
from gestioncof.management.base import MyBaseCommand
from gestioncof.models import CofProfile
from kfet.models import (Account, Article, OperationGroup, Operation,
Checkout, CheckoutStatement)
from kfet.models import (Account, Checkout, CheckoutStatement, Supplier,
SupplierArticle, Article)
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
@ -126,65 +126,24 @@ class Command(MyBaseCommand):
amount_error=0
)
# ---
# Fournisseur
# ---
supplier, created = Supplier.objects.get_or_create(name="Panoramix")
if created:
articles = random.sample(list(Article.objects.all()), 40)
to_create = []
for article in articles:
to_create.append(SupplierArticle(
supplier=supplier,
article=article
))
SupplierArticle.objects.bulk_create(to_create)
# ---
# Opérations
# ---
self.stdout.write("Génération d'opérations")
articles = Article.objects.all()
accounts = Account.objects.exclude(trigramme='LIQ')
num_op = 100
# Operations are put uniformly over the span of a week
past_date = 3600*24*7
for i in range(num_op):
if random.random() > 0.25:
account = random.choice(accounts)
else:
account = liq_account
amount = Decimal('0')
at = timezone.now() - timedelta(
seconds=random.randint(0, past_date))
opegroup = OperationGroup(
on_acc=account,
checkout=checkout,
at=at,
is_cof=account.cofprofile.is_cof)
opegroup.save()
for j in range(random.randint(1, 4)):
typevar = random.random()
if typevar > 0.9 and account != liq_account:
ope = Operation(
group=opegroup,
type=Operation.DEPOSIT,
amount=Decimal(random.randint(1, 99)/10,)
)
elif typevar > 0.8 and account != liq_account:
ope = Operation(
group=opegroup,
type=Operation.WITHDRAW,
amount=-Decimal(random.randint(1, 99)/10,)
)
else:
article = random.choice(articles)
nb = random.randint(1, 5)
ope = Operation(
group=opegroup,
type=Operation.PURCHASE,
amount=-article.price*nb,
article=article,
article_nb=nb
)
ope.save()
amount += ope.amount
opegroup.amount = amount
opegroup.save()
call_command('createopes', '100', '7', '--transfers=20')

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('kfet', '0047_auto_20170104_1528'),
]
operations = [
migrations.AlterField(
model_name='operationgroup',
name='at',
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AlterField(
model_name='transfergroup',
name='at',
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0048_article_hidden'),
('kfet', '0048_default_datetime'),
]
operations = [
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0049_merge'),
]
operations = [
migrations.RemoveField(
model_name='operation',
name='is_checkout',
),
migrations.AlterField(
model_name='operation',
name='type',
field=models.CharField(choices=[('purchase', 'Achat'), ('deposit', 'Charge'), ('withdraw', 'Retrait'), ('initial', 'Initial'), ('edit', 'Édition')], max_length=8),
),
]

View file

@ -40,7 +40,7 @@ class Account(models.Model):
balance = models.DecimalField(
max_digits = 6, decimal_places = 2,
default = 0)
is_frozen = models.BooleanField(default = False)
is_frozen = models.BooleanField("est gelé", default = False)
created_at = models.DateTimeField(auto_now_add = True, null = True)
# Optional
PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)]
@ -48,6 +48,7 @@ class Account(models.Model):
choices = PROMO_CHOICES,
blank = True, null = True, default = default_promo())
nickname = models.CharField(
"surnom(s)",
max_length = 255,
blank = True, default = "")
password = models.CharField(
@ -224,14 +225,18 @@ class AccountNegative(models.Model):
start = models.DateTimeField(
blank = True, null = True, default = None)
balance_offset = models.DecimalField(
"décalage de balance",
help_text="Montant non compris dans l'autorisation de négatif",
max_digits = 6, decimal_places = 2,
blank = True, null = True, default = None)
authz_overdraft_amount = models.DecimalField(
"négatif autorisé",
max_digits = 6, decimal_places = 2,
blank = True, null = True, default = None)
authz_overdraft_until = models.DateTimeField(
"expiration du négatif",
blank = True, null = True, default = None)
comment = models.CharField(max_length = 255, blank = True)
comment = models.CharField("commentaire", max_length = 255, blank = True)
@python_2_unicode_compatible
class Checkout(models.Model):
@ -273,29 +278,35 @@ class CheckoutStatement(models.Model):
checkout = models.ForeignKey(
Checkout, on_delete = models.PROTECT,
related_name = "statements")
balance_old = models.DecimalField(max_digits = 6, decimal_places = 2)
balance_new = models.DecimalField(max_digits = 6, decimal_places = 2)
amount_taken = models.DecimalField(max_digits = 6, decimal_places = 2)
amount_error = models.DecimalField(max_digits = 6, decimal_places = 2)
balance_old = models.DecimalField("ancienne balance",
max_digits = 6, decimal_places = 2)
balance_new = models.DecimalField("nouvelle balance",
max_digits = 6, decimal_places = 2)
amount_taken = models.DecimalField("montant pris",
max_digits = 6, decimal_places = 2)
amount_error = models.DecimalField("montant de l'erreur",
max_digits = 6, decimal_places = 2)
at = models.DateTimeField(auto_now_add = True)
not_count = models.BooleanField(default=False)
not_count = models.BooleanField("caisse non comptée", default=False)
taken_001 = models.PositiveSmallIntegerField(default=0)
taken_002 = models.PositiveSmallIntegerField(default=0)
taken_005 = models.PositiveSmallIntegerField(default=0)
taken_01 = models.PositiveSmallIntegerField(default=0)
taken_02 = models.PositiveSmallIntegerField(default=0)
taken_05 = models.PositiveSmallIntegerField(default=0)
taken_1 = models.PositiveSmallIntegerField(default=0)
taken_2 = models.PositiveSmallIntegerField(default=0)
taken_5 = models.PositiveSmallIntegerField(default=0)
taken_10 = models.PositiveSmallIntegerField(default=0)
taken_20 = models.PositiveSmallIntegerField(default=0)
taken_50 = models.PositiveSmallIntegerField(default=0)
taken_100 = models.PositiveSmallIntegerField(default=0)
taken_200 = models.PositiveSmallIntegerField(default=0)
taken_500 = models.PositiveSmallIntegerField(default=0)
taken_cheque = models.DecimalField(default=0, max_digits=6, decimal_places=2)
taken_001 = models.PositiveSmallIntegerField("pièces de 1¢", default=0)
taken_002 = models.PositiveSmallIntegerField("pièces de 2¢", default=0)
taken_005 = models.PositiveSmallIntegerField("pièces de 5¢", default=0)
taken_01 = models.PositiveSmallIntegerField("pièces de 10¢", default=0)
taken_02 = models.PositiveSmallIntegerField("pièces de 20¢", default=0)
taken_05 = models.PositiveSmallIntegerField("pièces de 50¢", default=0)
taken_1 = models.PositiveSmallIntegerField("pièces de 1€", default=0)
taken_2 = models.PositiveSmallIntegerField("pièces de 2€", default=0)
taken_5 = models.PositiveSmallIntegerField("billets de 5€", default=0)
taken_10 = models.PositiveSmallIntegerField("billets de 10€", default=0)
taken_20 = models.PositiveSmallIntegerField("billets de 20€", default=0)
taken_50 = models.PositiveSmallIntegerField("billets de 50€", default=0)
taken_100 = models.PositiveSmallIntegerField("billets de 100€", default=0)
taken_200 = models.PositiveSmallIntegerField("billets de 200€", default=0)
taken_500 = models.PositiveSmallIntegerField("billets de 500€", default=0)
taken_cheque = models.DecimalField(
"montant des chèques",
default=0, max_digits=6, decimal_places=2)
def __str__(self):
return '%s %s' % (self.checkout, self.at)
@ -336,19 +347,21 @@ class ArticleCategory(models.Model):
@python_2_unicode_compatible
class Article(models.Model):
name = models.CharField(max_length = 45)
is_sold = models.BooleanField(default = True)
hidden = models.BooleanField(default=False,
name = models.CharField("nom", max_length = 45)
is_sold = models.BooleanField("en vente", default = True)
hidden = models.BooleanField("caché",
default=False,
help_text="Si oui, ne sera pas affiché "
"au public ; par exemple "
"sur la carte.")
price = models.DecimalField(
"prix",
max_digits = 6, decimal_places = 2,
default = 0)
stock = models.IntegerField(default = 0)
category = models.ForeignKey(
ArticleCategory, on_delete = models.PROTECT,
related_name = "articles")
related_name = "articles", verbose_name='catégorie')
BOX_TYPE_CHOICES = (
("caisse", "caisse"),
("carton", "carton"),
@ -356,10 +369,12 @@ class Article(models.Model):
("fût", "fût"),
)
box_type = models.CharField(
"type de contenant",
choices = BOX_TYPE_CHOICES,
max_length = choices_length(BOX_TYPE_CHOICES),
blank = True, null = True, default = None)
box_capacity = models.PositiveSmallIntegerField(
"capacité du contenant",
blank = True, null = True, default = None)
def __str__(self):
@ -417,11 +432,11 @@ class Supplier(models.Model):
Article,
through = 'SupplierArticle',
related_name = "suppliers")
name = models.CharField(max_length = 45)
address = models.TextField()
email = models.EmailField()
phone = models.CharField(max_length = 10)
comment = models.TextField()
name = models.CharField("nom", max_length = 45)
address = models.TextField("adresse")
email = models.EmailField("adresse mail")
phone = models.CharField("téléphone", max_length = 10)
comment = models.TextField("commentaire")
def __str__(self):
return self.name
@ -466,7 +481,7 @@ class OrderArticle(models.Model):
quantity_received = models.IntegerField(default = 0)
class TransferGroup(models.Model):
at = models.DateTimeField(auto_now_add = True)
at = models.DateTimeField(default=timezone.now)
# Optional
comment = models.CharField(
max_length = 255,
@ -502,7 +517,7 @@ class OperationGroup(models.Model):
checkout = models.ForeignKey(
Checkout, on_delete = models.PROTECT,
related_name = "opesgroup")
at = models.DateTimeField(auto_now_add = True)
at = models.DateTimeField(default=timezone.now)
amount = models.DecimalField(
max_digits = 6, decimal_places = 2,
default = 0)
@ -522,54 +537,63 @@ class OperationGroup(models.Model):
class Operation(models.Model):
PURCHASE = 'purchase'
DEPOSIT = 'deposit'
DEPOSIT = 'deposit'
WITHDRAW = 'withdraw'
INITIAL = 'initial'
EDIT = 'edit'
TYPE_ORDER_CHOICES = (
(PURCHASE, 'Achat'),
(DEPOSIT, 'Charge'),
(WITHDRAW, 'Retrait'),
(INITIAL, 'Initial'),
(EDIT, 'Édition'),
)
group = models.ForeignKey(
OperationGroup, on_delete = models.PROTECT,
related_name = "opes")
OperationGroup, on_delete=models.PROTECT,
related_name="opes")
type = models.CharField(
choices = TYPE_ORDER_CHOICES,
max_length = choices_length(TYPE_ORDER_CHOICES))
choices=TYPE_ORDER_CHOICES,
max_length=choices_length(TYPE_ORDER_CHOICES))
amount = models.DecimalField(
max_digits = 6, decimal_places = 2,
blank = True, default = 0)
is_checkout = models.BooleanField(default = True)
max_digits=6, decimal_places=2,
blank=True, default=0)
# Optional
article = models.ForeignKey(
Article, on_delete = models.PROTECT,
related_name = "operations",
blank = True, null = True, default = None)
Article, on_delete=models.PROTECT,
related_name="operations",
blank=True, null=True, default=None)
article_nb = models.PositiveSmallIntegerField(
blank = True, null = True, default = None)
blank=True, null=True, default=None)
canceled_by = models.ForeignKey(
Account, on_delete = models.PROTECT,
related_name = "+",
blank = True, null = True, default = None)
Account, on_delete=models.PROTECT,
related_name="+",
blank=True, null=True, default=None)
canceled_at = models.DateTimeField(
blank = True, null = True, default = None)
blank=True, null=True, default=None)
addcost_for = models.ForeignKey(
Account, on_delete = models.PROTECT,
related_name = "addcosts",
blank = True, null = True, default = None)
Account, on_delete=models.PROTECT,
related_name="addcosts",
blank=True, null=True, default=None)
addcost_amount = models.DecimalField(
max_digits = 6, decimal_places = 2,
blank = True, null = True, default = None)
max_digits=6, decimal_places=2,
blank=True, null=True, default=None)
@property
def is_checkout(self):
return (self.type == Operation.DEPOSIT or
self.type == Operation.WITHDRAW or
(self.type == Operation.PURCHASE and self.group.on_acc.is_cash)
)
def __str__(self):
templates = {
self.PURCHASE: "{nb} {article.name} ({amount}€)",
self.DEPOSIT: "charge ({amount})",
self.WITHDRAW: "retrait ({amount})",
self.INITIAL: "initial ({amount})",
self.DEPOSIT: "charge ({amount}€)",
self.WITHDRAW: "retrait ({amount}€)",
self.INITIAL: "initial ({amount}€)",
self.EDIT: "édition ({amount}€)",
}
return templates[self.type].format(nb=self.article_nb,
article=self.article,

View file

@ -31,13 +31,22 @@ function KHistory(options={}) {
if (ope['type'] == 'purchase') {
infos1 = ope['article_nb'];
infos2 = ope['article__name'];
} else if (ope['type'] == 'initial') {
infos1 = parsed_amount.toFixed(2)+'€';
infos2 = 'Initial';
} else {
infos1 = parsed_amount.toFixed(2)+'€';
infos2 = (ope['type'] == 'deposit') ? 'Charge' : 'Retrait';
infos2 = ope['is_checkout'] ? infos2 : 'Édition';
switch (ope['type']) {
case 'initial':
infos2 = 'Initial';
break;
case 'withdraw':
infos2 = 'Retrait';
break;
case 'deposit':
infos2 = 'Charge';
break;
case 'edit':
infos2 = 'Édition';
break;
}
}
$ope_html

View file

@ -27,6 +27,53 @@ $(document).ready(function() {
}
});
/*
* Generic Websocket class and k-psul ws instanciation
*/
class KfetWebsocket {
static get defaults() {
return {"relative_url": "", "default_msg": {}, "handlers": []};
}
constructor(data) {
$.extend(this, this.constructor.defaults, data);
}
get url() {
var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws';
var location_host = window.location.host;
var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host;
return websocket_protocol+"://" + location_url + this.relative_url ;
}
add_handler(handler) {
if (!this.socket)
this.listen();
this.handlers.push(handler);
}
listen() {
var that = this;
this.socket = new ReconnectingWebSocket(this.url);
this.socket.onmessage = function(e) {
var data = $.extend({}, that.default_msg, JSON.parse(e.data));
for (let handler of that.handlers) {
handler(data);
}
}
}
}
var OperationWebSocket = new KfetWebsocket({
'relative_url': '/ws/k-fet/k-psul/',
'default_msg': {'opegroups':[],'opes':[],'checkouts':[],'articles':[]},
});
function dateUTCToParis(date) {
return moment.tz(date, 'UTC').tz('Europe/Paris');
}
@ -86,6 +133,12 @@ function getErrorsHtml(data) {
content += '<li>Montant invalide</li>';
content += '</ul>';
}
if ('account' in data['errors']) {
content += 'Général';
content += '<ul>';
content += '<li>Opération invalide sur le compte '+data['errors']['account']+'</li>';
content += '</ul>';
}
return content;
}

View file

@ -1,17 +1,27 @@
{% extends 'kfet/base.html' %}
{% load widget_tweaks %}
{% load staticfiles %}
{% block title %}Nouvel article{% endblock %}
{% block content-header-title %}Création d'un article{% endblock %}
{% block content %}
<form submit="" method="post">
{% csrf_token %}
{{ form.as_p }}
{% if not perms.kfet.add_article %}
<input type="password" name="KFETPASSWORD">
{% endif %}
<input type="submit" value="Enregistrer">
</form>
{% include "kfet/base_messages.html" %}
<div class="row form-only">
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.add_article %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer" %}
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -101,9 +101,8 @@
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
<script>
jQuery(document).ready(function() {
var stat_last = $("#stat_last");
var stat_last_url = "{% url 'kfet.article.stat.last' article.id %}";
STAT.get_thing(stat_last_url, stat_last, "Stat non trouvées :(");
var stat_last = new StatsGroup("{% url 'kfet.article.stat.last' article.id %}",
$("#stat_last"));
});
</script>
{% endblock %}

View file

@ -1,17 +1,27 @@
{% extends 'kfet/base.html' %}
{% load widget_tweaks %}
{% load staticfiles %}
{% block title %}Édition de l'article {{ article.name }}{% endblock %}
{% block content-header-title %}Article {{ article.name }} - Édition{% endblock %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
{% if not perms.kfet.change_article %}
<input type="password" name="KFETPASSWORD">
{% endif %}
<input type="submit" value="Mettre à jour">
</form>
{% include "kfet/base_messages.html" %}
<div class="row form-only">
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_article %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -80,7 +80,7 @@
<td><input id="id_taken_001" name="taken_001" data-value="0.01" min="0" value="0" type="number" class="form-control" required></td>
</tr>
</table>
Chèque: <input id="id_taken_cheque" name="taken_cheque" data-value="1" min="0" step="0.01" value="0" type="number" class="form-control" required>
<p style="font-weight:bold"> Chèque:</p> <input id="id_taken_cheque" name="taken_cheque" data-value="1" min="0" step="0.01" value="0" type="number" class="form-control" required>
</div>
</div>
<div class="content-right-block">

View file

@ -15,15 +15,15 @@ Caisse {{ checkout.name }} - Modification relevé {{ checkoutstatement.at }}
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
<div class="content-right-block">
<form action="" method="post">
<div class="content-right form-only">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
{% csrf_token %}
{{ form.as_p }}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_checkoutstatement %}
<input type="password" name="KFETPASSWORD">
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
<input type="submit" value="Enregistrer">
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer" %}
</form>
</div>
</div>

View file

@ -4,6 +4,11 @@
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-10">
{{ field|add_class:'form-control' }}
<span class="help-block">{{ field.errors }}</span>
{% if field.errors %}
<span class="help-block">{{field.errors}}</span>
{% endif %}
{% if field.help_text %}
<span class="help-block">{{field.help_text}}</span>
{% endif %}
</div>
</div>

View file

@ -12,7 +12,6 @@
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %}
@ -876,15 +875,27 @@ $(document).ready(function() {
return (-5 <= stock - nb && stock - nb <= 5);
}
function addDeposit(amount, is_checkout=1) {
function addDeposit(amount) {
var deposit_basket_html = $(item_basket_default_html);
var amount = parseFloat(amount).toFixed(2);
var index = addDepositToFormset(amount, is_checkout);
var text = is_checkout ? 'Charge' : 'Édition';
var index = addDepositToFormset(amount);
deposit_basket_html
.attr('data-opeindex', index)
.find('.number').text(amount+"€").end()
.find('.name').text(text).end()
.find('.name').text('Charge').end()
.find('.amount').text(amountToUKF(amount, account_data['is_cof'], false));
basket_container.prepend(deposit_basket_html);
updateBasketRel();
}
function addEdit(amount) {
var deposit_basket_html = $(item_basket_default_html);
var amount = parseFloat(amount).toFixed(2);
var index = addEditToFormset(amount);
deposit_basket_html
.attr('data-opeindex', index)
.find('.number').text(amount+"€").end()
.find('.name').text('Édition').end()
.find('.amount').text(amountToUKF(amount, account_data['is_cof'], false));
basket_container.prepend(deposit_basket_html);
updateBasketRel();
@ -1047,11 +1058,10 @@ $(document).ready(function() {
// Ask deposit or withdraw
// -----
function askDeposit(is_checkout=1) {
var title = is_checkout ? 'Montant de la charge' : "Montant de l'édition";
function askDeposit() {
$.confirm({
title: title,
content: '<input type="number" lang="en" step="0.01" min="0.01" on autofocus placeholder="€">',
title: 'Montant de la charge',
content: '<input type="number" lang="en" step="0.01" min="0.01" autofocus placeholder="€">',
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
@ -1060,7 +1070,34 @@ $(document).ready(function() {
var amount = this.$content.find('input').val();
if (!$.isNumeric(amount) || amount <= 0)
return false;
addDeposit(amount, is_checkout);
addDeposit(amount);
},
onOpen: function() {
var that = this
this.$content.find('input').on('keydown', function(e) {
if (e.keyCode == 13) {
e.preventDefault();
that.$confirmButton.click();
}
});
},
onClose: function() { this._lastFocused = (articleSelect.val() ? articleNb : articleSelect) ; }
});
}
function askEdit() {
$.confirm({
title: "Montant de l'édition",
content: '<input type="number" lang="en" step="0.01" min="0.01" autofocus placeholder="€">',
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirm: function() {
var amount = this.$content.find('input').val();
if (!$.isNumeric(amount))
return false;
addEdit(amount);
},
onOpen: function() {
var that = this
@ -1078,7 +1115,7 @@ $(document).ready(function() {
function askWithdraw() {
$.confirm({
title: 'Montant du retrait',
content: '<input type="number" lang="en" step="0.01" min="0.01" on autofocus placeholder="€">',
content: '<input type="number" lang="en" step="0.01" min="0.01" autofocus placeholder="€">',
backgroundDismiss: true,
animation:'top',
closeAnimation:'bottom',
@ -1125,7 +1162,7 @@ $(document).ready(function() {
var mngmt_total_forms = 1;
var prefix_regex = /__prefix__/;
function addOperationToFormset(type, amount, article='', article_nb='', is_checkout=1) {
function addOperationToFormset(type, amount, article='', article_nb='') {
var operation_html = operation_empty_html.clone();
var index = mngmt_total_forms;
@ -1135,8 +1172,7 @@ $(document).ready(function() {
.find('#id_form-__prefix__-type').val(type).end()
.find('#id_form-__prefix__-amount').val((parseFloat(amount)).toFixed(2)).end()
.find('#id_form-__prefix__-article').val(article).end()
.find('#id_form-__prefix__-article_nb').val(article_nb).end()
.find('#id_form-__prefix__-is_checkout').val(is_checkout);
.find('#id_form-__prefix__-article_nb').val(article_nb).end();
mngmt_total_forms_input.val(index+1);
mngmt_total_forms++;
@ -1151,12 +1187,16 @@ $(document).ready(function() {
return index;
}
function addDepositToFormset(amount, is_checkout=1) {
return addOperationToFormset('deposit', amount, '', '', is_checkout);
function addDepositToFormset(amount) {
return addOperationToFormset('deposit', amount, '', '');
}
function addWithdrawToFormset(amount, is_checkout=1) {
return addOperationToFormset('withdraw', amount, '', '', is_checkout);
function addEditToFormset(amount) {
return addOperationToFormset('edit', amount, '', '');
}
function addWithdrawToFormset(amount) {
return addOperationToFormset('withdraw', amount, '', '');
}
function addPurchaseToFormset(article_id, article_nb, amount=0) {
@ -1321,15 +1361,7 @@ $(document).ready(function() {
// Synchronization
// -----
websocket_msg_default = {'opegroups':[],'opes':[],'checkouts':[],'articles':[]}
var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws';
var location_host = window.location.host;
var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host;
socket = new ReconnectingWebSocket(websocket_protocol+"://" + location_url + "/ws/k-fet/k-psul/");
socket.onmessage = function(e) {
data = $.extend({}, websocket_msg_default, JSON.parse(e.data));
OperationWebSocket.add_handler(function(data) {
for (var i=0; i<data['opegroups'].length; i++) {
if (data['opegroups'][i]['add']) {
khistory.addOpeGroup(data['opegroups'][i]);
@ -1349,18 +1381,26 @@ $(document).ready(function() {
}
}
for (var i=0; i<data['articles'].length; i++) {
article = data['articles'][i];
articles_container.find('#data-article-'+article['id'])
.addClass('low-stock');
articles_container.find('#data-article-'+article['id']+' .stock')
var article = data['articles'][i];
var article_line = articles_container.find('#data-article-'+article.id);
if (article.stock <= 5 && article.stock >= -5) {
article_line.addClass('low-stock');
} else {
article_line.removeClass('low-stock');
}
article_line.find('.stock')
.text(article['stock']);
var i = 0;
while (i < articlesList.length && articlesList[i][1] != article.id) i++ ;
articlesList[i][4] = article.stock ;
}
if (data['addcost']) {
settings['addcost_for'] = data['addcost']['for'];
settings['addcost_amount'] = parseFloat(data['addcost']['amount']);
displayAddcost();
}
}
});
// -----
// General
@ -1441,7 +1481,7 @@ $(document).ready(function() {
return false;
case 119:
// F8 - Edition
askDeposit(0);
askEdit();
return false;
case 120:
// F9 - Addcost

View file

@ -1,4 +1,5 @@
{% extends 'kfet/base.html' %}
{% load widget_tweaks %}
{% block title %}Nouvelle commande{% endblock %}
{% block content-header-title %}Nouvelle commande {{ supplier.name }}{% endblock %}
@ -60,7 +61,7 @@
<td>{{ form.v_prev }}</td>
<td>{{ form.stock }}</td>
<td>{{ form.c_rec }}</td>
<td>{{ form.quantity_ordered }}</td>
<td>{{ form.quantity_ordered | add_class:"form-control" }}</td>
</tr>
{% endfor %}
</tbody>

View file

@ -1,4 +1,5 @@
{% extends 'kfet/base.html' %}
{% load widget_tweaks %}
{% block title %}{% endblock %}
{% block content-header-title %}{% endblock %}
@ -33,11 +34,11 @@
<tr>
{{ form.article }}
<td>{{ form.name }}</td>
<td>{{ form.price_HT }}</td>
<td>{{ form.TVA }}</td>
<td>{{ form.rights }}</td>
<td>{{ form.price_HT | add_class:"form-control" }}</td>
<td>{{ form.TVA | add_class:"form-control" }}</td>
<td>{{ form.rights | add_class:"form-control" }}</td>
<td>{{ form.quantity_ordered }}</td>
<td>{{ form.quantity_received }}</td>
<td>{{ form.quantity_received | add_class:"form-control" }}</td>
{% endfor %}
</tbody>
</table>

View file

@ -1,4 +1,6 @@
{% extends 'kfet/base.html' %}
{% load widget_tweaks %}
{% load staticfiles %}
{% block title %}Fournisseur - Modification{% endblock %}
{% block content-header-title %}Fournisseur - Modification{% endblock %}
@ -7,13 +9,19 @@
{% include 'kfet/base_messages.html' %}
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
{% if not perms.kfet.change_supplier %}
<input type="password" name="KFETPASSWORD">
{% endif %}
<input type="submit" class="btn btn-primary btn-lg">
</form>
<div class="row form-only">
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_supplier %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -165,8 +165,7 @@ def account_create_special(request):
ope = Operation.objects.create(
group = opegroup,
type = Operation.INITIAL,
amount = amount,
is_checkout = False)
amount = amount)
messages.success(request, 'Compte créé : %s' % account.trigramme)
return redirect('kfet.account.create')
except Account.UserHasAccount as e:
@ -1010,10 +1009,7 @@ def kpsul_perform_operations(request):
operation.amount -= operation.addcost_amount
to_addcost_for_balance += operation.addcost_amount
if operationgroup.on_acc.is_cash:
operation.is_checkout = True
to_checkout_balance += -operation.amount
else:
operation.is_checkout = False
if operationgroup.on_acc.is_cof:
if is_addcost:
operation.addcost_amount = operation.addcost_amount / cof_grant_divisor
@ -1021,13 +1017,13 @@ def kpsul_perform_operations(request):
to_articles_stocks[operation.article] -= operation.article_nb
else:
if operationgroup.on_acc.is_cash:
data['errors']['account'] = 'Charge et retrait impossible sur LIQ'
to_checkout_balance += operation.amount
data['errors']['account'] = 'LIQ'
if operation.type != Operation.EDIT:
to_checkout_balance += operation.amount
operationgroup.amount += operation.amount
if operation.type == Operation.DEPOSIT:
required_perms.add('kfet.perform_deposit')
if (not operation.is_checkout
and operation.type in [Operation.DEPOSIT, Operation.WITHDRAW]):
if operation.type == Operation.EDIT:
required_perms.add('kfet.edit_balance_account')
need_comment = True
if operationgroup.on_acc.is_cof:
@ -1041,7 +1037,9 @@ def kpsul_perform_operations(request):
operationgroup.comment = operationgroup.comment.strip()
if not operationgroup.comment:
data['errors']['need_comment'] = True
return JsonResponse(data, status=400)
if data['errors']:
return JsonResponse(data, status=400)
if stop or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
@ -1122,8 +1120,7 @@ def kpsul_perform_operations(request):
ope_data = {
'id': operation.pk, 'type': operation.type, 'amount': operation.amount,
'addcost_amount': operation.addcost_amount,
'addcost_for__trigramme': is_addcost and addcost_for.trigramme or None,
'is_checkout': operation.is_checkout,
'addcost_for__trigramme': operation.addcost_for and addcost_for.trigramme or None,
'article__name': operation.article and operation.article.name or None,
'article_nb': operation.article_nb,
'group_id': operationgroup.pk,
@ -1213,11 +1210,11 @@ def kpsul_cancel_operations(request):
.order_by('at')
.last())
if not last_statement or last_statement.at < ope.group.at:
if ope.type == Operation.PURCHASE:
if ope.is_checkout:
if ope.group.on_acc.is_cash:
to_checkouts_balances[ope.group.checkout] -= - ope.amount
else:
to_checkouts_balances[ope.group.checkout] -= ope.amount
else:
to_checkouts_balances[ope.group.checkout] -= ope.amount
# Pour les stocks d'articles
# Les stocks d'articles dont il y a eu un inventaire depuis la date
@ -1378,7 +1375,6 @@ def history_json(request):
'type' : ope.type,
'amount' : ope.amount,
'article_nb' : ope.article_nb,
'is_checkout' : ope.is_checkout,
'addcost_amount': ope.addcost_amount,
'canceled_at' : ope.canceled_at,
'article__name':