Order create use Scale.
Order create view use WeekScale. No query improvements, only shorter code. Scale/ScaleMixin: - Two methods directly relative to the Scale class move to... the Scale class. - Fix order create on Chrome.
This commit is contained in:
parent
3f4a1adbb9
commit
06572f0bb5
|
@ -88,6 +88,99 @@ class Scale(object):
|
||||||
label_fmt = self.label_fmt
|
label_fmt = self.label_fmt
|
||||||
return [begin.strftime(label_fmt) for begin, end in self]
|
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):
|
class DayScale(Scale):
|
||||||
name = 'day'
|
name = 'day'
|
||||||
|
@ -229,97 +322,3 @@ class ScaleMixin(object):
|
||||||
|
|
||||||
def get_default_scale(self):
|
def get_default_scale(self):
|
||||||
return DayScale(n_steps=7, last=True)
|
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
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_by_chunks(self, qs, scale, field_callback=None, field_db='at'):
|
|
||||||
"""Objects of queryset ranked according to a given 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`.
|
|
||||||
scale: Used to rank objects.
|
|
||||||
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: scale.begin, end_f: scale.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 scale:
|
|
||||||
# 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)
|
|
||||||
|
|
|
@ -161,6 +161,8 @@ $(document).ready(function() {
|
||||||
$('input[type="submit"]').on("click", function(e) {
|
$('input[type="submit"]').on("click", function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
var content;
|
||||||
|
|
||||||
if (conflicts.size) {
|
if (conflicts.size) {
|
||||||
content = '';
|
content = '';
|
||||||
content += "Conflits possibles :"
|
content += "Conflits possibles :"
|
||||||
|
|
|
@ -50,7 +50,7 @@ from decimal import Decimal
|
||||||
import django_cas_ng
|
import django_cas_ng
|
||||||
import heapq
|
import heapq
|
||||||
import statistics
|
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):
|
class Home(TemplateView):
|
||||||
|
@ -1798,68 +1798,60 @@ class OrderList(ListView):
|
||||||
context['suppliers'] = Supplier.objects.order_by('name')
|
context['suppliers'] = Supplier.objects.order_by('name')
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@teamkfet_required
|
@teamkfet_required
|
||||||
def order_create(request, pk):
|
def order_create(request, pk):
|
||||||
supplier = get_object_or_404(Supplier, pk=pk)
|
supplier = get_object_or_404(Supplier, pk=pk)
|
||||||
|
|
||||||
articles = (Article.objects
|
articles = (
|
||||||
|
Article.objects
|
||||||
.filter(suppliers=supplier.pk)
|
.filter(suppliers=supplier.pk)
|
||||||
.distinct()
|
.distinct()
|
||||||
.select_related('category')
|
.select_related('category')
|
||||||
.order_by('category__name', 'name'))
|
.order_by('category__name', 'name')
|
||||||
|
)
|
||||||
|
|
||||||
initial = []
|
# Force hit to cache
|
||||||
today = timezone.now().date()
|
articles = list(articles)
|
||||||
sales_q = (Operation.objects
|
|
||||||
|
sales_q = (
|
||||||
|
Operation.objects
|
||||||
.select_related('group')
|
.select_related('group')
|
||||||
.filter(article__in=articles, canceled_at=None)
|
.filter(article__in=articles, canceled_at=None)
|
||||||
.values('article'))
|
.values('article')
|
||||||
sales_s1 = (sales_q
|
|
||||||
.filter(
|
|
||||||
group__at__gte = today-timedelta(weeks=5),
|
|
||||||
group__at__lt = today-timedelta(weeks=4))
|
|
||||||
.annotate(nb=Sum('article_nb'))
|
.annotate(nb=Sum('article_nb'))
|
||||||
)
|
)
|
||||||
sales_s1 = { d['article']:d['nb'] for d in sales_s1 }
|
scale = WeekScale(last=True, n_steps=5, std_chunk=False)
|
||||||
sales_s2 = (sales_q
|
chunks = scale.chunkify_qs(sales_q, field='group__at')
|
||||||
.filter(
|
sales = []
|
||||||
group__at__gte = today-timedelta(weeks=4),
|
|
||||||
group__at__lt = today-timedelta(weeks=3))
|
for chunk in chunks:
|
||||||
.annotate(nb=Sum('article_nb'))
|
sales.append(
|
||||||
|
{d['article']: d['nb'] for d in chunk}
|
||||||
)
|
)
|
||||||
sales_s2 = { d['article']:d['nb'] for d in sales_s2 }
|
|
||||||
sales_s3 = (sales_q
|
initial = []
|
||||||
.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 }
|
|
||||||
|
|
||||||
for article in articles:
|
for article in articles:
|
||||||
v_s1 = sales_s1.get(article.pk, 0)
|
# Get sales for each 5 last weeks
|
||||||
v_s2 = sales_s2.get(article.pk, 0)
|
v_s1 = sales[0].get(article.pk, 0)
|
||||||
v_s3 = sales_s3.get(article.pk, 0)
|
v_s2 = sales[1].get(article.pk, 0)
|
||||||
v_s4 = sales_s4.get(article.pk, 0)
|
v_s3 = sales[2].get(article.pk, 0)
|
||||||
v_s5 = sales_s5.get(article.pk, 0)
|
v_s4 = sales[3].get(article.pk, 0)
|
||||||
|
v_s5 = sales[4].get(article.pk, 0)
|
||||||
v_all = [v_s1, v_s2, v_s3, v_s4, v_s5]
|
v_all = [v_s1, v_s2, v_s3, v_s4, v_s5]
|
||||||
|
# Take the 3 greatest (eg to avoid 2 weeks of vacations)
|
||||||
v_3max = heapq.nlargest(3, v_all)
|
v_3max = heapq.nlargest(3, v_all)
|
||||||
|
# Get average and standard deviation
|
||||||
v_moy = statistics.mean(v_3max)
|
v_moy = statistics.mean(v_3max)
|
||||||
v_et = statistics.pstdev(v_3max, v_moy)
|
v_et = statistics.pstdev(v_3max, v_moy)
|
||||||
|
# Expected sales for next week
|
||||||
v_prev = v_moy + v_et
|
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)
|
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:
|
if article.box_capacity:
|
||||||
c_rec_temp = c_rec_tot / article.box_capacity
|
c_rec_temp = c_rec_tot / article.box_capacity
|
||||||
if c_rec_temp >= 10:
|
if c_rec_temp >= 10:
|
||||||
|
@ -1890,7 +1882,8 @@ def order_create(request, pk):
|
||||||
|
|
||||||
cls_formset = formset_factory(
|
cls_formset = formset_factory(
|
||||||
form=OrderArticleForm,
|
form=OrderArticleForm,
|
||||||
extra = 0)
|
extra=0,
|
||||||
|
)
|
||||||
|
|
||||||
if request.POST:
|
if request.POST:
|
||||||
formset = cls_formset(request.POST, initial=initial)
|
formset = cls_formset(request.POST, initial=initial)
|
||||||
|
@ -1907,14 +1900,15 @@ def order_create(request, pk):
|
||||||
order.save()
|
order.save()
|
||||||
saved = True
|
saved = True
|
||||||
|
|
||||||
article = articles.get(pk=form.cleaned_data['article'].pk)
|
article = form.cleaned_data['article']
|
||||||
q_ordered = form.cleaned_data['quantity_ordered']
|
q_ordered = form.cleaned_data['quantity_ordered']
|
||||||
if article.box_capacity:
|
if article.box_capacity:
|
||||||
q_ordered *= article.box_capacity
|
q_ordered *= article.box_capacity
|
||||||
OrderArticle.objects.create(
|
OrderArticle.objects.create(
|
||||||
order=order,
|
order=order,
|
||||||
article=article,
|
article=article,
|
||||||
quantity_ordered = q_ordered)
|
quantity_ordered=q_ordered,
|
||||||
|
)
|
||||||
if saved:
|
if saved:
|
||||||
messages.success(request, 'Commande créée')
|
messages.success(request, 'Commande créée')
|
||||||
return redirect('kfet.order.read', order.pk)
|
return redirect('kfet.order.read', order.pk)
|
||||||
|
@ -1929,6 +1923,7 @@ def order_create(request, pk):
|
||||||
'formset': formset,
|
'formset': formset,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class OrderRead(DetailView):
|
class OrderRead(DetailView):
|
||||||
model = Order
|
model = Order
|
||||||
template_name = 'kfet/order_read.html'
|
template_name = 'kfet/order_read.html'
|
||||||
|
@ -2378,8 +2373,8 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
|
||||||
)
|
)
|
||||||
if types is not None:
|
if types is not None:
|
||||||
all_operations = all_operations.filter(type__in=types)
|
all_operations = all_operations.filter(type__in=types)
|
||||||
chunks = self.get_by_chunks(
|
chunks = scale.get_by_chunks(
|
||||||
all_operations, scale, field_db='group__at',
|
all_operations, field_db='group__at',
|
||||||
field_callback=(lambda d: d['group__at']),
|
field_callback=(lambda d: d['group__at']),
|
||||||
)
|
)
|
||||||
return chunks
|
return chunks
|
||||||
|
@ -2462,12 +2457,12 @@ class ArticleStatSales(ScaleMixin, JSONDetailView):
|
||||||
liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ')
|
liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ')
|
||||||
liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ')
|
liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ')
|
||||||
|
|
||||||
chunks_liq = self.get_by_chunks(
|
chunks_liq = scale.get_by_chunks(
|
||||||
liq_only, scale, field_db='group__at',
|
liq_only, field_db='group__at',
|
||||||
field_callback=lambda d: d['group__at'],
|
field_callback=lambda d: d['group__at'],
|
||||||
)
|
)
|
||||||
chunks_no_liq = self.get_by_chunks(
|
chunks_no_liq = scale.get_by_chunks(
|
||||||
liq_exclude, scale, field_db='group__at',
|
liq_exclude, field_db='group__at',
|
||||||
field_callback=lambda d: d['group__at'],
|
field_callback=lambda d: d['group__at'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue