Merge branch 'master' into aureplop/clean_scroll

This commit is contained in:
Aurélien Delobelle 2017-05-19 14:08:57 +02:00
commit ecce2fda21
24 changed files with 896 additions and 427 deletions

View file

@ -1,15 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
import hashlib
from django.contrib.auth.models import User, Permission
from gestioncof.models import CofProfile
from kfet.models import Account, GenericTeamToken
class KFetBackend(object):
def authenticate(self, request):
password = request.POST.get('KFETPASSWORD', '')
@ -18,13 +15,15 @@ class KFetBackend(object):
return None
try:
password_sha256 = hashlib.sha256(password.encode('utf-8')).hexdigest()
password_sha256 = (
hashlib.sha256(password.encode('utf-8'))
.hexdigest()
)
account = Account.objects.get(password=password_sha256)
user = account.cofprofile.user
return account.cofprofile.user
except Account.DoesNotExist:
return None
return user
class GenericTeamBackend(object):
def authenticate(self, username=None, token=None):
@ -46,6 +45,10 @@ class GenericTeamBackend(object):
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
return (
User.objects
.select_related('profile__account_kfet')
.get(pk=user_id)
)
except User.DoesNotExist:
return None

View file

@ -134,6 +134,7 @@ class UserRestrictTeamForm(UserForm):
class Meta(UserForm.Meta):
fields = ['first_name', 'last_name', 'email']
class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt'),
@ -141,14 +142,12 @@ class UserGroupForm(forms.ModelForm):
required=False)
def clean_groups(self):
groups = self.cleaned_data.get('groups')
# Si aucun groupe, on le dénomme
if not groups:
groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return groups
kfet_groups = self.cleaned_data.get('groups')
other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return list(kfet_groups) + list(other_groups)
class Meta:
model = User
model = User
fields = ['groups']
@ -343,12 +342,20 @@ class KPsulAccountForm(forms.ModelForm):
}),
}
class KPsulCheckoutForm(forms.Form):
checkout = forms.ModelChoiceField(
queryset=Checkout.objects.filter(
is_protected=False, valid_from__lte=timezone.now(),
valid_to__gte=timezone.now()),
widget=forms.Select(attrs={'id':'id_checkout_select'}))
queryset=(
Checkout.objects
.filter(
is_protected=False,
valid_from__lte=timezone.now(),
valid_to__gte=timezone.now(),
)
),
widget=forms.Select(attrs={'id': 'id_checkout_select'}),
)
class KPsulOperationForm(forms.ModelForm):
article = forms.ModelChoiceField(

View file

@ -14,7 +14,8 @@ from kfet.models import (Account, Article, OperationGroup, Operation,
class Command(BaseCommand):
help = "Crée des opérations réparties uniformément sur une période de temps"
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
@ -29,7 +30,6 @@ class Command(BaseCommand):
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")
@ -44,6 +44,7 @@ class Command(BaseCommand):
# Convert to seconds
time = options['days'] * 24 * 3600
now = timezone.now()
checkout = Checkout.objects.first()
articles = Article.objects.all()
accounts = Account.objects.exclude(trigramme='LIQ')
@ -55,6 +56,13 @@ class Command(BaseCommand):
except Account.DoesNotExist:
con_account = random.choice(accounts)
# use to fetch OperationGroup pk created by bulk_create
at_list = []
# use to lazy set OperationGroup pk on Operation objects
ope_by_grp = []
# OperationGroup objects to bulk_create
opegroup_list = []
for i in range(num_ops):
# Randomly pick account
@ -64,8 +72,7 @@ class Command(BaseCommand):
account = liq_account
# Randomly pick time
at = timezone.now() - timedelta(
seconds=random.randint(0, time))
at = now - timedelta(seconds=random.randint(0, time))
# Majoration sur compte 'concert'
if random.random() < 0.2:
@ -78,13 +85,6 @@ class Command(BaseCommand):
# 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)):
@ -94,25 +94,26 @@ class Command(BaseCommand):
# 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
# 0.05 probability to have a withdrawal
elif typevar > 0.85 and account != liq_account:
ope = Operation(
type=Operation.WITHDRAW,
amount=-Decimal(random.randint(1, 99)/10)
)
# 0.05 probability to have an edition
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)
type=Operation.EDIT,
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,
@ -129,17 +130,44 @@ class Command(BaseCommand):
ope_list.append(ope)
amount += ope.amount
Operation.objects.bulk_create(ope_list)
opes_created += len(ope_list)
opegroup.amount = amount
opegroup.save()
opegroup_list.append(OperationGroup(
on_acc=account,
checkout=checkout,
at=at,
is_cof=account.cofprofile.is_cof,
amount=amount,
))
at_list.append(at)
ope_by_grp.append((at, ope_list, ))
OperationGroup.objects.bulk_create(opegroup_list)
# Fetch created OperationGroup objects pk by at
opegroups = (OperationGroup.objects
.filter(at__in=at_list)
.values('id', 'at'))
opegroups_by = {grp['at']: grp['id'] for grp in opegroups}
all_ope = []
for _ in range(num_ops):
at, ope_list = ope_by_grp.pop()
for ope in ope_list:
ope.group_id = opegroups_by[at]
all_ope.append(ope)
Operation.objects.bulk_create(all_ope)
opes_created = len(all_ope)
# Transfer generation
transfer_by_grp = []
transfergroup_list = []
at_list = []
for i in range(num_transfers):
# Randomly pick time
at = timezone.now() - timedelta(
seconds=random.randint(0, time))
at = now - timedelta(seconds=random.randint(0, time))
# Choose whether to have a comment
if random.random() > 0.5:
@ -147,24 +175,40 @@ class Command(BaseCommand):
else:
comment = ""
transfergroup = TransferGroup.objects.create(
transfergroup_list.append(TransferGroup(
at=at,
comment=comment,
valid_by=random.choice(accounts)
)
valid_by=random.choice(accounts),
))
at_list.append(at)
# 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)
transfer_by_grp.append((at, transfer_list, ))
TransferGroup.objects.bulk_create(transfergroup_list)
transfergroups = (TransferGroup.objects
.filter(at__in=at_list)
.values('id', 'at'))
transfergroups_by = {grp['at']: grp['id'] for grp in transfergroups}
all_transfer = []
for _ in range(num_transfers):
at, transfer_list = transfer_by_grp.pop()
for transfer in transfer_list:
transfer.group_id = transfergroups_by[at]
all_transfer.append(transfer)
Transfer.objects.bulk_create(all_transfer)
transfers += len(all_transfer)
self.stdout.write(
"- {:d} opérations créées dont {:d} commandes d'articles"

View file

@ -1,15 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.contrib.auth.models import User
from django.http import HttpResponseForbidden
from kfet.backends import KFetBackend
from kfet.models import Account
class KFetAuthenticationMiddleware(object):
"""Authenticate another user for this request if KFetBackend succeeds.
By the way, if a user is authenticated, we refresh its from db to add
values from CofProfile and Account of this user.
"""
def process_request(self, request):
if request.user.is_authenticated():
# avoid multiple db accesses in views and templates
user_pk = request.user.pk
request.user = (
User.objects
.select_related('profile__account_kfet')
.get(pk=user_pk)
)
kfet_backend = KFetBackend()
temp_request_user = kfet_backend.authenticate(request)
if temp_request_user:

View file

@ -23,8 +23,19 @@ def default_promo():
now = date.today()
return now.month <= 8 and now.year-1 or now.year
@python_2_unicode_compatible
class AccountManager(models.Manager):
"""Manager for Account Model."""
def get_queryset(self):
"""Always append related data to this Account."""
return super().get_queryset().select_related('cofprofile__user',
'negative')
class Account(models.Model):
objects = AccountManager()
cofprofile = models.OneToOneField(
CofProfile, on_delete = models.PROTECT,
related_name = "account_kfet")
@ -237,27 +248,43 @@ class Account(models.Model):
def __init__(self, trigramme):
self.trigramme = trigramme
class AccountNegativeManager(models.Manager):
"""Manager for AccountNegative model."""
def get_queryset(self):
return (
super().get_queryset()
.select_related('account__cofprofile__user')
)
class AccountNegative(models.Model):
objects = AccountNegativeManager()
account = models.OneToOneField(
Account, on_delete = models.PROTECT,
related_name = "negative")
start = models.DateTimeField(
blank = True, null = True, default = None)
Account, on_delete=models.PROTECT,
related_name="negative",
)
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)
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)
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("commentaire", max_length = 255, blank = True)
blank=True, null=True, default=None,
)
comment = models.CharField("commentaire", max_length=255, blank=True)
@python_2_unicode_compatible
class Checkout(models.Model):
created_by = models.ForeignKey(
Account, on_delete = models.PROTECT,

View file

@ -61,7 +61,7 @@
var chart = charts[i];
// format the data
var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1);
var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 0);
chart_datasets.push(
{
@ -132,7 +132,7 @@
type: 'line',
options: chart_options,
data: {
labels: (data.labels || []).slice(1),
labels: data.labels || [],
datasets: chart_datasets,
}
};

View file

@ -4,6 +4,7 @@ from datetime import date, datetime, time, timedelta
from dateutil.relativedelta import relativedelta
from dateutil.parser import parse as dateutil_parse
import pytz
from django.utils import timezone
from django.db.models import Sum
@ -13,7 +14,8 @@ KFET_WAKES_UP_AT = time(7, 0)
def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
"""datetime wrapper with time offset."""
return datetime.combine(date(year, month, day), start_at)
naive = datetime.combine(date(year, month, day), start_at)
return pytz.timezone('Europe/Paris').localize(naive, is_dst=None)
def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT):
@ -32,16 +34,21 @@ class Scale(object):
self.std_chunk = std_chunk
if last:
end = timezone.now()
if std_chunk:
if begin is not None:
begin = self.get_chunk_start(begin)
if end is not None:
end = self.do_step(self.get_chunk_start(end))
if begin is not None and n_steps != 0:
self.begin = self.get_from(begin)
self.begin = begin
self.end = self.do_step(self.begin, n_steps=n_steps)
elif end is not None and n_steps != 0:
self.end = self.get_from(end)
self.end = end
self.begin = self.do_step(self.end, n_steps=-n_steps)
elif begin is not None and end is not None:
self.begin = self.get_from(begin)
self.end = self.get_from(end)
self.begin = begin
self.end = end
else:
raise Exception('Two of these args must be specified: '
'n_steps, begin, end; '
@ -71,7 +78,7 @@ class Scale(object):
def get_datetimes(self):
datetimes = [self.begin]
tmp = self.begin
while tmp <= self.end:
while tmp < self.end:
tmp = self.do_step(tmp)
datetimes.append(tmp)
return datetimes
@ -81,6 +88,99 @@ class Scale(object):
label_fmt = self.label_fmt
return [begin.strftime(label_fmt) for begin, end in self]
def chunkify_qs(self, qs, field=None):
if field is None:
field = 'at'
begin_f = '{}__gte'.format(field)
end_f = '{}__lte'.format(field)
return [
qs.filter(**{begin_f: begin, end_f: end})
for begin, end in self
]
def get_by_chunks(self, qs, field_callback=None, field_db='at'):
"""Objects of queryset ranked according to the scale.
Returns a generator whose each item, corresponding to a scale chunk,
is a generator of objects from qs for this chunk.
Args:
qs: Queryset of source objects, must be ordered *first* on the
same field returned by `field_callback`.
field_callback: Callable which gives value from an object used
to compare against limits of the scale chunks.
Default to: lambda obj: getattr(obj, field_db)
field_db: Used to filter against `scale` limits.
Default to 'at'.
Examples:
If queryset `qs` use `values()`, `field_callback` must be set and
could be: `lambda d: d['at']`
If `field_db` use foreign attributes (eg with `__`), it should be
something like: `lambda obj: obj.group.at`.
"""
if field_callback is None:
def field_callback(obj):
return getattr(obj, field_db)
begin_f = '{}__gte'.format(field_db)
end_f = '{}__lte'.format(field_db)
qs = (
qs
.filter(**{begin_f: self.begin, end_f: self.end})
)
obj_iter = iter(qs)
last_obj = None
def _objects_until(obj_iter, field_callback, end):
"""Generator of objects until `end`.
Ends if objects source is empty or when an object not verifying
field_callback(obj) <= end is met.
If this object exists, it is stored in `last_obj` which is found
from outer scope.
Also, if this same variable is non-empty when the function is
called, it first yields its content.
Args:
obj_iter: Source used to get objects.
field_callback: Returned value, when it is called on an object
will be used to test ordering against `end`.
end
"""
nonlocal last_obj
if last_obj is not None:
yield last_obj
last_obj = None
for obj in obj_iter:
if field_callback(obj) <= end:
yield obj
else:
last_obj = obj
return
for begin, end in self:
# forward last seen object, if it exists, to the right chunk,
# and fill with empty generators for intermediate chunks of scale
if last_obj is not None:
if field_callback(last_obj) > end:
yield iter(())
continue
# yields generator for this chunk
# this set last_obj to None if obj_iter reach its end, otherwise
# it's set to the first met object from obj_iter which doesn't
# belong to this chunk
yield _objects_until(obj_iter, field_callback, end)
class DayScale(Scale):
name = 'day'
@ -222,13 +322,3 @@ class ScaleMixin(object):
def get_default_scale(self):
return DayScale(n_steps=7, last=True)
def chunkify_qs(self, qs, scale, field=None):
if field is None:
field = 'at'
begin_f = '{}__gte'.format(field)
end_f = '{}__lte'.format(field)
return [
qs.filter(**{begin_f: begin, end_f: end})
for begin, end in scale
]

View file

@ -104,7 +104,7 @@
$(document).ready(function() {
var stat_last = new StatsGroup(
"{% url 'kfet.article.stat.sales.list' article.id %}",
$("#stat_last"),
$("#stat_last")
);
});
</script>

View file

@ -196,6 +196,8 @@ $(document).ready(function() {
$('input[type="submit"]').on("click", function(e) {
e.preventDefault();
var content;
if (conflicts.size) {
content = '';
content += "Conflits possibles :"

View file

@ -182,7 +182,7 @@ $(document).ready(function() {
settings = {}
function resetSettings() {
$.ajax({
return $.ajax({
dataType: "json",
url : "{% url 'kfet.kpsul.get_settings' %}",
method : "POST",
@ -191,7 +191,6 @@ $(document).ready(function() {
settings['addcost_for'] = data['addcost_for'];
settings['addcost_amount'] = parseFloat(data['addcost_amount']);
settings['subvention_cof'] = parseFloat(data['subvention_cof']);
displayAddcost();
});
}
@ -393,6 +392,11 @@ $(document).ready(function() {
var last_statement_container = $('#last_statement');
var last_statement_html_default = '<b>Dernier relevé: </b><br><span id="checkout-last_statement_balance"></span>€ le <span id="checkout-last_statement_at"></span> par <span id="checkout-last_statement_by_trigramme"></span>';
// If only one checkout is available, select it
var checkout_choices = checkoutInput.find("option[value!='']");
if (checkout_choices.length == 1) {
$(checkout_choices[0]).prop("selected", true);
}
// Display data
function displayCheckoutData() {
@ -1426,9 +1430,11 @@ $(document).ready(function() {
resetArticles();
resetPreviousOp();
khistory.reset();
resetSettings();
getArticles();
getHistory();
resetSettings().done(function (){
getArticles();
getHistory();
displayAddcost();
});
}
function resetSelectable() {

56
kfet/tests/test_forms.py Normal file
View file

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from django.test import TestCase
from django.contrib.auth.models import User, Group
from kfet.forms import UserGroupForm
class UserGroupFormTests(TestCase):
"""Test suite for UserGroupForm."""
def setUp(self):
# create user
self.user = User.objects.create(username="foo", password="foo")
# create some K-Fêt groups
prefix_name = "K-Fêt "
names = ["Group 1", "Group 2", "Group 3"]
self.kfet_groups = [
Group.objects.create(name=prefix_name+name)
for name in names
]
# create a non-K-Fêt group
self.other_group = Group.objects.create(name="Other group")
def test_choices(self):
"""Only K-Fêt groups are selectable."""
form = UserGroupForm(instance=self.user)
groups_field = form.fields['groups']
self.assertQuerysetEqual(
groups_field.queryset,
[repr(g) for g in self.kfet_groups],
ordered=False,
)
def test_keep_others(self):
"""User stays in its non-K-Fêt groups."""
user = self.user
# add user to a non-K-Fêt group
user.groups.add(self.other_group)
# add user to some K-Fêt groups through UserGroupForm
data = {
'groups': [group.pk for group in self.kfet_groups],
}
form = UserGroupForm(data, instance=user)
form.is_valid()
form.save()
self.assertQuerysetEqual(
user.groups.all(),
[repr(g) for g in [self.other_group] + self.kfet_groups],
ordered=False,
)

View file

@ -50,18 +50,28 @@ from decimal import Decimal
import django_cas_ng
import heapq
import statistics
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale
class Home(TemplateView):
template_name = "kfet/home.html"
def get_context_data(self, **kwargs):
context = super(TemplateView, self).get_context_data(**kwargs)
articles = Article.objects.all().filter(is_sold=True, hidden=False)
context['pressions'] = articles.filter(category__name='Pression')
context['articles'] = (articles.exclude(category__name='Pression')
.order_by('category'))
context = super().get_context_data(**kwargs)
articles = list(
Article.objects
.filter(is_sold=True, hidden=False)
.select_related('category')
.order_by('category__name')
)
pressions, others = [], []
while len(articles) > 0:
article = articles.pop()
if article.category.name == 'Pression':
pressions.append(article)
else:
others.append(article)
context['pressions'], context['articles'] = pressions, others
return context
@method_decorator(login_required)
@ -355,25 +365,22 @@ def account_create_ajax(request, username=None, login_clipper=None,
@login_required
def account_read(request, trigramme):
try:
account = (Account.objects.select_related('negative')
.get(trigramme=trigramme))
except Account.DoesNotExist:
raise Http404
account = get_object_or_404(Account, trigramme=trigramme)
# Checking permissions
if not request.user.has_perm('kfet.is_team') \
and request.user != account.user:
raise PermissionDenied
addcosts = (OperationGroup.objects
.filter(opes__addcost_for=account,
opes__canceled_at=None)
.extra({'date': "date(at)"})
.values('date')
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
.order_by('-date')
)
addcosts = (
OperationGroup.objects
.filter(opes__addcost_for=account,
opes__canceled_at=None)
.extra({'date': "date(at)"})
.values('date')
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
.order_by('-date')
)
return render(request, "kfet/account_read.html", {
'account': account,
@ -524,13 +531,22 @@ def account_update(request, trigramme):
'pwd_form': pwd_form,
})
@permission_required('kfet.manage_perms')
def account_group(request):
groups = (Group.objects
.filter(name__icontains='K-Fêt')
.prefetch_related('permissions', 'user_set__profile__account_kfet')
user_pre = Prefetch(
'user_set',
queryset=User.objects.select_related('profile__account_kfet'),
)
return render(request, 'kfet/account_group.html', { 'groups': groups })
groups = (
Group.objects
.filter(name__icontains='K-Fêt')
.prefetch_related('permissions', user_pre)
)
return render(request, 'kfet/account_group.html', {
'groups': groups,
})
class AccountGroupCreate(SuccessMessageMixin, CreateView):
model = Group
@ -546,23 +562,20 @@ class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
success_message = 'Groupe modifié : %(name)s'
success_url = reverse_lazy('kfet.account.group')
class AccountNegativeList(ListView):
queryset = (AccountNegative.objects
queryset = (
AccountNegative.objects
.select_related('account', 'account__cofprofile__user')
.exclude(account__trigramme='#13'))
.exclude(account__trigramme='#13')
)
template_name = 'kfet/account_negative.html'
context_object_name = 'negatives'
def get_context_data(self, **kwargs):
context = super(AccountNegativeList, self).get_context_data(**kwargs)
negs_sum = (AccountNegative.objects
.exclude(account__trigramme='#13')
.aggregate(
bal = Coalesce(Sum('account__balance'),0),
offset = Coalesce(Sum('balance_offset'),0),
)
)
context['negatives_sum'] = negs_sum['bal'] - negs_sum['offset']
real_balances = (neg.account.real_balance for neg in self.object_list)
context['negatives_sum'] = sum(real_balances)
return context
# -----
@ -765,12 +778,18 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView):
# Article - General
class ArticleList(ListView):
queryset = (Article.objects
.select_related('category')
.prefetch_related(Prefetch('inventories',
queryset=Inventory.objects.order_by('-at'),
to_attr='inventory'))
.order_by('category', '-is_sold', 'name'))
queryset = (
Article.objects
.select_related('category')
.prefetch_related(
Prefetch(
'inventories',
queryset=Inventory.objects.order_by('-at'),
to_attr='inventory',
)
)
.order_by('category__name', '-is_sold', 'name')
)
template_name = 'kfet/article.html'
context_object_name = 'articles'
@ -882,7 +901,6 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
return super(ArticleUpdate, self).form_valid(form)
# -----
# K-Psul
# -----
@ -892,17 +910,10 @@ def kpsul(request):
data = {}
data['operationgroup_form'] = KPsulOperationGroupForm()
data['trigramme_form'] = KPsulAccountForm()
initial = {}
try:
checkout = Checkout.objects.filter(
is_protected=False, valid_from__lte=timezone.now(),
valid_to__gte=timezone.now()).get()
initial['checkout'] = checkout
except (Checkout.DoesNotExist, Checkout.MultipleObjectsReturned):
pass
data['checkout_form'] = KPsulCheckoutForm(initial=initial)
operation_formset = KPsulOperationFormSet(queryset=Operation.objects.none())
data['operation_formset'] = operation_formset
data['checkout_form'] = KPsulCheckoutForm()
data['operation_formset'] = KPsulOperationFormSet(
queryset=Operation.objects.none(),
)
return render(request, 'kfet/kpsul.html', data)
@ -1483,19 +1494,32 @@ class SettingsUpdate(SuccessMessageMixin, FormView):
return super().form_valid(form)
# -----
# Transfer views
# -----
@teamkfet_required
def transfers(request):
transfergroups = (TransferGroup.objects
.prefetch_related('transfers')
.order_by('-at'))
transfers_pre = Prefetch(
'transfers',
queryset=(
Transfer.objects
.select_related('from_acc', 'to_acc')
),
)
transfergroups = (
TransferGroup.objects
.select_related('valid_by')
.prefetch_related(transfers_pre)
.order_by('-at')
)
return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups,
})
@teamkfet_required
def transfers_create(request):
transfer_formset = TransferFormSet(queryset=Transfer.objects.none())
@ -1639,9 +1663,6 @@ def cancel_transfers(request):
if stop:
negative_accounts.append(account.trigramme)
print(required_perms)
print(request.user.get_all_permissions())
if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms:
@ -1779,68 +1800,54 @@ class OrderList(ListView):
context['suppliers'] = Supplier.objects.order_by('name')
return context
@teamkfet_required
def order_create(request, pk):
supplier = get_object_or_404(Supplier, pk=pk)
articles = (Article.objects
.filter(suppliers=supplier.pk)
.distinct()
.select_related('category')
.order_by('category__name', 'name'))
articles = (
Article.objects
.filter(suppliers=supplier.pk)
.distinct()
.select_related('category')
.order_by('category__name', 'name')
)
initial = []
today = timezone.now().date()
sales_q = (Operation.objects
# Force hit to cache
articles = list(articles)
sales_q = (
Operation.objects
.select_related('group')
.filter(article__in=articles, canceled_at=None)
.values('article'))
sales_s1 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=5),
group__at__lt = today-timedelta(weeks=4))
.values('article')
.annotate(nb=Sum('article_nb'))
)
sales_s1 = { d['article']:d['nb'] for d in sales_s1 }
sales_s2 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=4),
group__at__lt = today-timedelta(weeks=3))
.annotate(nb=Sum('article_nb'))
)
sales_s2 = { d['article']:d['nb'] for d in sales_s2 }
sales_s3 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=3),
group__at__lt = today-timedelta(weeks=2))
.annotate(nb=Sum('article_nb'))
)
sales_s3 = { d['article']:d['nb'] for d in sales_s3 }
sales_s4 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=2),
group__at__lt = today-timedelta(weeks=1))
.annotate(nb=Sum('article_nb'))
)
sales_s4 = { d['article']:d['nb'] for d in sales_s4 }
sales_s5 = (sales_q
.filter(group__at__gte = today-timedelta(weeks=1))
.annotate(nb=Sum('article_nb'))
)
sales_s5 = { d['article']:d['nb'] for d in sales_s5 }
scale = WeekScale(last=True, n_steps=5, std_chunk=False)
chunks = scale.chunkify_qs(sales_q, field='group__at')
sales = [
{d['article']: d['nb'] for d in chunk}
for chunk in chunks
]
initial = []
for article in articles:
v_s1 = sales_s1.get(article.pk, 0)
v_s2 = sales_s2.get(article.pk, 0)
v_s3 = sales_s3.get(article.pk, 0)
v_s4 = sales_s4.get(article.pk, 0)
v_s5 = sales_s5.get(article.pk, 0)
v_all = [v_s1, v_s2, v_s3, v_s4, v_s5]
# Get sales for each 5 last weeks
v_all = [chunk.get(article.pk, 0) for chunk in sales]
# Take the 3 greatest (eg to avoid 2 weeks of vacations)
v_3max = heapq.nlargest(3, v_all)
# Get average and standard deviation
v_moy = statistics.mean(v_3max)
v_et = statistics.pstdev(v_3max, v_moy)
# Expected sales for next week
v_prev = v_moy + v_et
# We want to have 1.5 * the expected sales in stock
# (because sometimes some articles are not delivered)
c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
# If ordered quantity is close enough to a level which can led to free
# boxes, we increase it to this level.
if article.box_capacity:
c_rec_temp = c_rec_tot / article.box_capacity
if c_rec_temp >= 10:
@ -1858,11 +1865,7 @@ def order_create(request, pk):
'category__name': article.category.name,
'stock': article.stock,
'box_capacity': article.box_capacity,
'v_s1': v_s1,
'v_s2': v_s2,
'v_s3': v_s3,
'v_s4': v_s4,
'v_s5': v_s5,
'v_all': v_all,
'v_moy': round(v_moy),
'v_et': round(v_et),
'v_prev': round(v_prev),
@ -1870,8 +1873,9 @@ def order_create(request, pk):
})
cls_formset = formset_factory(
form = OrderArticleForm,
extra = 0)
form=OrderArticleForm,
extra=0,
)
if request.POST:
formset = cls_formset(request.POST, initial=initial)
@ -1888,14 +1892,15 @@ def order_create(request, pk):
order.save()
saved = True
article = articles.get(pk=form.cleaned_data['article'].pk)
article = form.cleaned_data['article']
q_ordered = form.cleaned_data['quantity_ordered']
if article.box_capacity:
q_ordered *= article.box_capacity
OrderArticle.objects.create(
order = order,
article = article,
quantity_ordered = q_ordered)
order=order,
article=article,
quantity_ordered=q_ordered,
)
if saved:
messages.success(request, 'Commande créée')
return redirect('kfet.order.read', order.pk)
@ -1907,9 +1912,10 @@ def order_create(request, pk):
return render(request, 'kfet/order_create.html', {
'supplier': supplier,
'formset' : formset,
'formset': formset,
})
class OrderRead(DetailView):
model = Order
template_name = 'kfet/order_read.html'
@ -1948,6 +1954,7 @@ class OrderRead(DetailView):
context['mail'] = mail
return context
@teamkfet_required
def order_to_inventory(request, pk):
order = get_object_or_404(Order, pk=pk)
@ -1955,28 +1962,36 @@ def order_to_inventory(request, pk):
if hasattr(order, 'inventory'):
raise Http404
articles = (Article.objects
.filter(orders=order.pk)
.select_related('category')
.prefetch_related(Prefetch('orderarticle_set',
queryset = OrderArticle.objects.filter(order=order),
to_attr = 'order'))
.prefetch_related(Prefetch('supplierarticle_set',
queryset = (SupplierArticle.objects
.filter(supplier=order.supplier)
.order_by('-at')),
to_attr = 'supplier'))
.order_by('category__name', 'name'))
supplier_prefetch = Prefetch(
'article__supplierarticle_set',
queryset=(
SupplierArticle.objects
.filter(supplier=order.supplier)
.order_by('-at')
),
to_attr='supplier',
)
order_articles = (
OrderArticle.objects
.filter(order=order.pk)
.select_related('article', 'article__category')
.prefetch_related(
supplier_prefetch,
)
.order_by('article__category__name', 'article__name')
)
initial = []
for article in articles:
for order_article in order_articles:
article = order_article.article
initial.append({
'article': article.pk,
'name': article.name,
'category': article.category_id,
'category__name': article.category.name,
'quantity_ordered': article.order[0].quantity_ordered,
'quantity_received': article.order[0].quantity_ordered,
'quantity_ordered': order_article.quantity_ordered,
'quantity_received': order_article.quantity_ordered,
'price_HT': article.supplier[0].price_HT,
'TVA': article.supplier[0].TVA,
'rights': article.supplier[0].rights,
@ -1991,31 +2006,50 @@ def order_to_inventory(request, pk):
messages.error(request, 'Permission refusée')
elif formset.is_valid():
with transaction.atomic():
inventory = Inventory()
inventory.order = order
inventory.by = request.user.profile.account_kfet
inventory.save()
inventory = Inventory.objects.create(
order=order, by=request.user.profile.account_kfet,
)
new_supplierarticle = []
new_inventoryarticle = []
for form in formset:
q_received = form.cleaned_data['quantity_received']
article = form.cleaned_data['article']
SupplierArticle.objects.create(
supplier = order.supplier,
article = article,
price_HT = form.cleaned_data['price_HT'],
TVA = form.cleaned_data['TVA'],
rights = form.cleaned_data['rights'])
(OrderArticle.objects
.filter(order=order, article=article)
.update(quantity_received = q_received))
InventoryArticle.objects.create(
inventory = inventory,
article = article,
stock_old = article.stock,
stock_new = article.stock + q_received)
price_HT = form.cleaned_data['price_HT']
TVA = form.cleaned_data['TVA']
rights = form.cleaned_data['rights']
if any((form.initial['price_HT'] != price_HT,
form.initial['TVA'] != TVA,
form.initial['rights'] != rights)):
new_supplierarticle.append(
SupplierArticle(
supplier=order.supplier,
article=article,
price_HT=price_HT,
TVA=TVA,
rights=rights,
)
)
(
OrderArticle.objects
.filter(order=order, article=article)
.update(quantity_received=q_received)
)
new_inventoryarticle.append(
InventoryArticle(
inventory=inventory,
article=article,
stock_old=article.stock,
stock_new=article.stock + q_received,
)
)
article.stock += q_received
if q_received > 0:
article.is_sold = True
article.save()
SupplierArticle.objects.bulk_create(new_supplierarticle)
InventoryArticle.objects.bulk_create(new_inventoryarticle)
messages.success(request, "C'est tout bon !")
return redirect('kfet.order')
else:
@ -2200,10 +2234,13 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
# prepare querysets
# TODO: retirer les opgroup dont tous les op sont annulées
opegroups = OperationGroup.objects.filter(on_acc=account)
recv_transfers = Transfer.objects.filter(to_acc=account,
canceled_at=None)
sent_transfers = Transfer.objects.filter(from_acc=account,
canceled_at=None)
transfers = (
Transfer.objects
.filter(canceled_at=None)
.select_related('group')
)
recv_transfers = transfers.filter(to_acc=account)
sent_transfers = transfers.filter(from_acc=account)
# apply filters
if begin_date is not None:
@ -2231,13 +2268,11 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
actions.append({
'at': (begin_date or account.created_at).isoformat(),
'amount': 0,
'label': 'début',
'balance': 0,
})
actions.append({
'at': (end_date or timezone.now()).isoformat(),
'amount': 0,
'label': 'fin',
'balance': 0,
})
@ -2245,21 +2280,18 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
{
'at': ope_grp.at.isoformat(),
'amount': ope_grp.amount,
'label': str(ope_grp),
'balance': 0,
} for ope_grp in opegroups
] + [
{
'at': tr.group.at.isoformat(),
'amount': tr.amount,
'label': str(tr),
'balance': 0,
} for tr in recv_transfers
] + [
{
'at': tr.group.at.isoformat(),
'amount': -tr.amount,
'label': str(tr),
'balance': 0,
} for tr in sent_transfers
]
@ -2352,13 +2384,19 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
# à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps
all_operations = (Operation.objects
.filter(group__on_acc=self.object)
.filter(canceled_at=None)
)
all_operations = (
Operation.objects
.filter(group__on_acc=self.object,
canceled_at=None)
.values('article_nb', 'group__at')
.order_by('group__at')
)
if types is not None:
all_operations = all_operations.filter(type__in=types)
chunks = self.chunkify_qs(all_operations, scale, field='group__at')
chunks = scale.get_by_chunks(
all_operations, field_db='group__at',
field_callback=(lambda d: d['group__at']),
)
return chunks
def get_context_data(self, *args, **kwargs):
@ -2374,7 +2412,8 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
# On compte les opérations
nb_ventes = []
for chunk in operations:
nb_ventes.append(tot_ventes(chunk))
ventes = sum(ope['article_nb'] for ope in chunk)
nb_ventes.append(ventes)
context['charts'] = [{"color": "rgb(255, 99, 132)",
"label": "NB items achetés",
@ -2425,29 +2464,39 @@ class ArticleStatSales(ScaleMixin, JSONDetailView):
context = {'labels': old_ctx['labels']}
scale = self.scale
# On selectionne les opérations qui correspondent
# à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps
all_operations = (
all_purchases = (
Operation.objects
.filter(type=Operation.PURCHASE,
article=self.object,
canceled_at=None,
)
.filter(
type=Operation.PURCHASE,
article=self.object,
canceled_at=None,
)
.values('group__at', 'article_nb')
.order_by('group__at')
)
chunks = self.chunkify_qs(all_operations, scale, field='group__at')
liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ')
liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ')
chunks_liq = scale.get_by_chunks(
liq_only, field_db='group__at',
field_callback=lambda d: d['group__at'],
)
chunks_no_liq = scale.get_by_chunks(
liq_exclude, field_db='group__at',
field_callback=lambda d: d['group__at'],
)
# On compte les opérations
nb_ventes = []
nb_accounts = []
nb_liq = []
for qs in chunks:
nb_ventes.append(
tot_ventes(qs))
nb_liq.append(
tot_ventes(qs.filter(group__on_acc__trigramme='LIQ')))
nb_accounts.append(
tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ')))
for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq):
sum_accounts = sum(ope['article_nb'] for ope in chunk_no_liq)
sum_liq = sum(ope['article_nb'] for ope in chunk_liq)
nb_ventes.append(sum_accounts + sum_liq)
nb_accounts.append(sum_accounts)
nb_liq.append(sum_liq)
context['charts'] = [{"color": "rgb(255, 99, 132)",
"label": "Toutes consommations",
"values": nb_ventes},