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") fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
tokens = models.TextField("Graine(s) du tirage", blank=True) tokens = models.TextField("Graine(s) du tirage", blank=True)
active = models.BooleanField("Tirage actif", default=False) 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é", enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
default=False) default=False)
@ -78,6 +79,15 @@ class Spectacle(models.Model):
self.price 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): def send_rappel(self):
""" """
Envoie un mail de rappel à toutes les personnes qui ont une place pour 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 # Envoie un mail aux perdants
for inscrit in inscrits: for inscrit in inscrits:
if inscrit != winner: if inscrit != winner:
context['acheteur'] = inscrit.user new_context = dict(context)
new_context['acheteur'] = inscrit.user
datatuple.append(( datatuple.append((
'bda-revente-loser', 'bda-revente-loser',
context, new_context,
settings.MAIL_DATA['revente']['FROM'], settings.MAIL_DATA['revente']['FROM'],
[inscrit.user.email] [inscrit.user.email]
)) ))

View file

@ -47,4 +47,6 @@ urlpatterns = [
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel), url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles, url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'), 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 random
import hashlib import hashlib
import time import time
import json
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import ( from custommail.shortcuts import (
send_mass_custom_mail, send_custom_mail, render_custom_mail send_mass_custom_mail, send_custom_mail, render_custom_mail
) )
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
@ -15,18 +15,24 @@ from django.db import models, transaction
from django.core import serializers from django.core import serializers
from django.db.models import Count, Q, Sum from django.db.models import Count, Q, Sum
from django.forms.models import inlineformset_factory 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.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.utils import timezone, formats from django.utils import timezone, formats
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.core.exceptions import ObjectDoesNotExist
from gestioncof.decorators import cof_required, buro_required from gestioncof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\ from bda.models import (
Tirage, SpectacleRevente Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
SpectacleRevente, Salle, Quote, CategorieSpectacle
)
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\ from bda.forms import (
InscriptionReventeForm, SoldForm BaseBdaFormSet, TokenForm, ResellForm, AnnulForm, InscriptionReventeForm,
SoldForm
)
@cof_required @cof_required
@ -639,3 +645,98 @@ def descriptions_spectacles(request, tirage_id):
return HttpResponseBadRequest( return HttpResponseBadRequest(
"La variable GET 'location' doit contenir un entier") "La variable GET 'location' doit contenir un entier")
return render(request, 'descriptions.html', {'shows': shows_qs.all()}) 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): class Clipper(object):
def __init__(self, clipper, fullname): def __init__(self, clipper, fullname):
if fullname is None:
fullname = ""
assert isinstance(clipper, str)
assert isinstance(fullname, str)
self.clipper = clipper self.clipper = clipper
self.fullname = fullname self.fullname = fullname
@ -62,17 +66,19 @@ def autocomplete(request):
)) ))
if ldap_query != "(&)": if ldap_query != "(&)":
# If none of the bits were legal, we do not perform the query # If none of the bits were legal, we do not perform the query
entries = None
with Connection(settings.LDAP_SERVER_URL) as conn: with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search( conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query, 'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn'] attributes=['uid', 'cn']
) )
queries['clippers'] = conn.entries entries = conn.entries
# Clearing redundancies # Clearing redundancies
queries['clippers'] = [ queries['clippers'] = [
Clipper(clipper.uid, clipper.cn) Clipper(entry.uid.value, entry.cn.value)
for clipper in queries['clippers'] for entry in entries
if str(clipper.uid) not in usernames if entry.uid.value
and entry.uid.value not in usernames
] ]
# Resulting data # Resulting data

View file

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

View file

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

View file

@ -74,8 +74,11 @@ class AccountRestrictForm(AccountForm):
class AccountPwdForm(forms.Form): class AccountPwdForm(forms.Form):
pwd1 = forms.CharField( 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) widget=forms.PasswordInput)
pwd2 = forms.CharField( pwd2 = forms.CharField(
label="Confirmer le mot de passe",
widget=forms.PasswordInput) widget=forms.PasswordInput)
def clean(self): def clean(self):
@ -128,6 +131,7 @@ class UserRestrictTeamForm(UserForm):
class UserGroupForm(forms.ModelForm): class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField( groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt'), Group.objects.filter(name__icontains='K-Fêt'),
label='Statut équipe',
required=False) required=False)
def clean_groups(self): def clean_groups(self):
@ -235,16 +239,20 @@ class CheckoutStatementUpdateForm(forms.ModelForm):
class ArticleForm(forms.ModelForm): class ArticleForm(forms.ModelForm):
category_new = forms.CharField( category_new = forms.CharField(
label="Créer une catégorie",
max_length=45, max_length=45,
required = False) required = False)
category = forms.ModelChoiceField( category = forms.ModelChoiceField(
label="Catégorie",
queryset = ArticleCategory.objects.all(), queryset = ArticleCategory.objects.all(),
required = False) required = False)
suppliers = forms.ModelMultipleChoiceField( suppliers = forms.ModelMultipleChoiceField(
label="Fournisseurs",
queryset = Supplier.objects.all(), queryset = Supplier.objects.all(),
required = False) required = False)
supplier_new = forms.CharField( supplier_new = forms.CharField(
label="Créer un fournisseur",
max_length = 45, max_length = 45,
required = False) required = False)
@ -318,11 +326,10 @@ class KPsulOperationForm(forms.ModelForm):
widget = forms.HiddenInput()) widget = forms.HiddenInput())
class Meta: class Meta:
model = Operation model = Operation
fields = ['type', 'amount', 'is_checkout', 'article', 'article_nb'] fields = ['type', 'amount', 'article', 'article_nb']
widgets = { widgets = {
'type': forms.HiddenInput(), 'type': forms.HiddenInput(),
'amount': forms.HiddenInput(), 'amount': forms.HiddenInput(),
'is_checkout': forms.HiddenInput(),
'article_nb': forms.HiddenInput(), 'article_nb': forms.HiddenInput(),
} }
@ -338,7 +345,6 @@ class KPsulOperationForm(forms.ModelForm):
"Un achat nécessite un article et une quantité") "Un achat nécessite un article et une quantité")
if article_nb < 1: if article_nb < 1:
raise ValidationError("Impossible d'acheter moins de 1 article") 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]: elif type_ope and type_ope in [Operation.DEPOSIT, Operation.WITHDRAW]:
if not amount or article or article_nb: if not amount or article or article_nb:
raise ValidationError("Bad request") raise ValidationError("Bad request")
@ -479,9 +485,7 @@ class OrderArticleForm(forms.Form):
queryset=Article.objects.all(), queryset=Article.objects.all(),
widget=forms.HiddenInput(), widget=forms.HiddenInput(),
) )
quantity_ordered = forms.IntegerField( quantity_ordered = forms.IntegerField(required=False)
required=False,
widget=forms.NumberInput(attrs={'class': 'form-control'}))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(OrderArticleForm, self).__init__(*args, **kwargs) super(OrderArticleForm, self).__init__(*args, **kwargs)
@ -508,18 +512,14 @@ class OrderArticleToInventoryForm(forms.Form):
) )
price_HT = forms.DecimalField( price_HT = forms.DecimalField(
max_digits = 7, decimal_places = 4, max_digits = 7, decimal_places = 4,
required = False, required = False)
widget=forms.NumberInput(attrs={'class': 'form-control'}))
TVA = forms.DecimalField( TVA = forms.DecimalField(
max_digits = 7, decimal_places = 2, max_digits = 7, decimal_places = 2,
required = False, required = False)
widget=forms.NumberInput(attrs={'class': 'form-control'}))
rights = forms.DecimalField( rights = forms.DecimalField(
max_digits = 7, decimal_places = 4, max_digits = 7, decimal_places = 4,
required = False, required = False)
widget=forms.NumberInput(attrs={'class': 'form-control'})) quantity_received = forms.IntegerField()
quantity_received = forms.IntegerField(
widget=forms.NumberInput(attrs={'class': 'form-control'}))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(OrderArticleToInventoryForm, self).__init__(*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 os
import random import random
from datetime import timedelta from datetime import timedelta
from decimal import Decimal
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.models import User, Group, Permission, ContentType 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.management.base import MyBaseCommand
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from kfet.models import (Account, Article, OperationGroup, Operation, from kfet.models import (Account, Checkout, CheckoutStatement, Supplier,
Checkout, CheckoutStatement) SupplierArticle, Article)
# Où sont stockés les fichiers json # Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
@ -126,65 +126,24 @@ class Command(MyBaseCommand):
amount_error=0 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 # Opérations
# --- # ---
self.stdout.write("Génération d'opérations") call_command('createopes', '100', '7', '--transfers=20')
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()

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

View file

@ -31,13 +31,22 @@ function KHistory(options={}) {
if (ope['type'] == 'purchase') { if (ope['type'] == 'purchase') {
infos1 = ope['article_nb']; infos1 = ope['article_nb'];
infos2 = ope['article__name']; infos2 = ope['article__name'];
} else if (ope['type'] == 'initial') {
infos1 = parsed_amount.toFixed(2)+'€';
infos2 = 'Initial';
} else { } else {
infos1 = parsed_amount.toFixed(2)+'€'; infos1 = parsed_amount.toFixed(2)+'€';
infos2 = (ope['type'] == 'deposit') ? 'Charge' : 'Retrait'; switch (ope['type']) {
infos2 = ope['is_checkout'] ? infos2 : 'Édition'; case 'initial':
infos2 = 'Initial';
break;
case 'withdraw':
infos2 = 'Retrait';
break;
case 'deposit':
infos2 = 'Charge';
break;
case 'edit':
infos2 = 'Édition';
break;
}
} }
$ope_html $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) { function dateUTCToParis(date) {
return moment.tz(date, 'UTC').tz('Europe/Paris'); return moment.tz(date, 'UTC').tz('Europe/Paris');
} }
@ -86,6 +133,12 @@ function getErrorsHtml(data) {
content += '<li>Montant invalide</li>'; content += '<li>Montant invalide</li>';
content += '</ul>'; 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; return content;
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,11 @@
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label> <label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
{{ field|add_class:'form-control' }} {{ 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>
</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.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.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/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> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %} {% endblock %}
@ -876,15 +875,27 @@ $(document).ready(function() {
return (-5 <= stock - nb && stock - nb <= 5); 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 deposit_basket_html = $(item_basket_default_html);
var amount = parseFloat(amount).toFixed(2); var amount = parseFloat(amount).toFixed(2);
var index = addDepositToFormset(amount, is_checkout); var index = addDepositToFormset(amount);
var text = is_checkout ? 'Charge' : 'Édition';
deposit_basket_html deposit_basket_html
.attr('data-opeindex', index) .attr('data-opeindex', index)
.find('.number').text(amount+"€").end() .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)); .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false));
basket_container.prepend(deposit_basket_html); basket_container.prepend(deposit_basket_html);
updateBasketRel(); updateBasketRel();
@ -1047,11 +1058,10 @@ $(document).ready(function() {
// Ask deposit or withdraw // Ask deposit or withdraw
// ----- // -----
function askDeposit(is_checkout=1) { function askDeposit() {
var title = is_checkout ? 'Montant de la charge' : "Montant de l'édition";
$.confirm({ $.confirm({
title: title, title: 'Montant de la charge',
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, backgroundDismiss: true,
animation:'top', animation:'top',
closeAnimation:'bottom', closeAnimation:'bottom',
@ -1060,7 +1070,34 @@ $(document).ready(function() {
var amount = this.$content.find('input').val(); var amount = this.$content.find('input').val();
if (!$.isNumeric(amount) || amount <= 0) if (!$.isNumeric(amount) || amount <= 0)
return false; 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() { onOpen: function() {
var that = this var that = this
@ -1078,7 +1115,7 @@ $(document).ready(function() {
function askWithdraw() { function askWithdraw() {
$.confirm({ $.confirm({
title: 'Montant du retrait', 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, backgroundDismiss: true,
animation:'top', animation:'top',
closeAnimation:'bottom', closeAnimation:'bottom',
@ -1125,7 +1162,7 @@ $(document).ready(function() {
var mngmt_total_forms = 1; var mngmt_total_forms = 1;
var prefix_regex = /__prefix__/; 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 operation_html = operation_empty_html.clone();
var index = mngmt_total_forms; var index = mngmt_total_forms;
@ -1135,8 +1172,7 @@ $(document).ready(function() {
.find('#id_form-__prefix__-type').val(type).end() .find('#id_form-__prefix__-type').val(type).end()
.find('#id_form-__prefix__-amount').val((parseFloat(amount)).toFixed(2)).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').val(article).end()
.find('#id_form-__prefix__-article_nb').val(article_nb).end() .find('#id_form-__prefix__-article_nb').val(article_nb).end();
.find('#id_form-__prefix__-is_checkout').val(is_checkout);
mngmt_total_forms_input.val(index+1); mngmt_total_forms_input.val(index+1);
mngmt_total_forms++; mngmt_total_forms++;
@ -1151,12 +1187,16 @@ $(document).ready(function() {
return index; return index;
} }
function addDepositToFormset(amount, is_checkout=1) { function addDepositToFormset(amount) {
return addOperationToFormset('deposit', amount, '', '', is_checkout); return addOperationToFormset('deposit', amount, '', '');
} }
function addWithdrawToFormset(amount, is_checkout=1) { function addEditToFormset(amount) {
return addOperationToFormset('withdraw', amount, '', '', is_checkout); return addOperationToFormset('edit', amount, '', '');
}
function addWithdrawToFormset(amount) {
return addOperationToFormset('withdraw', amount, '', '');
} }
function addPurchaseToFormset(article_id, article_nb, amount=0) { function addPurchaseToFormset(article_id, article_nb, amount=0) {
@ -1321,15 +1361,7 @@ $(document).ready(function() {
// Synchronization // Synchronization
// ----- // -----
websocket_msg_default = {'opegroups':[],'opes':[],'checkouts':[],'articles':[]} OperationWebSocket.add_handler(function(data) {
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));
for (var i=0; i<data['opegroups'].length; i++) { for (var i=0; i<data['opegroups'].length; i++) {
if (data['opegroups'][i]['add']) { if (data['opegroups'][i]['add']) {
khistory.addOpeGroup(data['opegroups'][i]); khistory.addOpeGroup(data['opegroups'][i]);
@ -1349,18 +1381,26 @@ $(document).ready(function() {
} }
} }
for (var i=0; i<data['articles'].length; i++) { for (var i=0; i<data['articles'].length; i++) {
article = data['articles'][i]; var article = data['articles'][i];
articles_container.find('#data-article-'+article['id']) var article_line = articles_container.find('#data-article-'+article.id);
.addClass('low-stock'); if (article.stock <= 5 && article.stock >= -5) {
articles_container.find('#data-article-'+article['id']+' .stock') article_line.addClass('low-stock');
} else {
article_line.removeClass('low-stock');
}
article_line.find('.stock')
.text(article['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']) { if (data['addcost']) {
settings['addcost_for'] = data['addcost']['for']; settings['addcost_for'] = data['addcost']['for'];
settings['addcost_amount'] = parseFloat(data['addcost']['amount']); settings['addcost_amount'] = parseFloat(data['addcost']['amount']);
displayAddcost(); displayAddcost();
} }
} });
// ----- // -----
// General // General
@ -1441,7 +1481,7 @@ $(document).ready(function() {
return false; return false;
case 119: case 119:
// F8 - Edition // F8 - Edition
askDeposit(0); askEdit();
return false; return false;
case 120: case 120:
// F9 - Addcost // F9 - Addcost

View file

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

View file

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

View file

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

View file

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