Ajout livraison

- Possible de passer une livraison à un fournisseur
- Proposition de quantités générées à partir des ventes sur les 5
  dernières semaines
- Mail généré à partir d'une commande (pas d'envoi auto)
- box_capacity et box_type passe de SupplierArticle à Article
This commit is contained in:
Aurélien Delobelle 2016-08-28 05:39:34 +02:00
parent d531c7dd5b
commit 61feb9bbcd
9 changed files with 391 additions and 28 deletions

View file

@ -218,11 +218,13 @@ class ArticleForm(forms.ModelForm):
class Meta: class Meta:
model = Article model = Article
fields = ['name', 'is_sold', 'price', 'stock', 'category'] fields = ['name', 'is_sold', 'price', 'stock', 'category', 'box_type',
'box_capacity']
class ArticleRestrictForm(ArticleForm): class ArticleRestrictForm(ArticleForm):
class Meta(ArticleForm.Meta): class Meta(ArticleForm.Meta):
fields = ['name', 'is_sold', 'price', 'category'] fields = ['name', 'is_sold', 'price', 'category', 'box_type',
'box_capacity']
# ----- # -----
# K-Psul forms # K-Psul forms
@ -416,3 +418,32 @@ class InventoryArticleForm(forms.Form):
self.stock_old = kwargs['initial']['stock_old'] self.stock_old = kwargs['initial']['stock_old']
self.category = kwargs['initial']['category'] self.category = kwargs['initial']['category']
self.category_name = kwargs['initial']['category__name'] self.category_name = kwargs['initial']['category__name']
# -----
# Order forms
# -----
class OrderArticleForm(forms.Form):
article = forms.ModelChoiceField(
queryset = Article.objects.all(),
widget = forms.HiddenInput(),
)
quantity_ordered = forms.IntegerField(required = False)
def __init__(self, *args, **kwargs):
super(OrderArticleForm, self).__init__(*args, **kwargs)
if 'initial' in kwargs:
self.name = kwargs['initial']['name']
self.stock = kwargs['initial']['stock']
self.category = kwargs['initial']['category']
self.category_name = kwargs['initial']['category__name']
self.box_capacity = kwargs['initial']['box_capacity']
self.v_s1 = kwargs['initial']['v_s1']
self.v_s2 = kwargs['initial']['v_s2']
self.v_s3 = kwargs['initial']['v_s3']
self.v_s4 = kwargs['initial']['v_s4']
self.v_s5 = kwargs['initial']['v_s5']
self.v_moy = kwargs['initial']['v_moy']
self.v_et = kwargs['initial']['v_et']
self.v_prev = kwargs['initial']['v_prev']
self.c_rec = kwargs['initial']['c_rec']

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('kfet', '0037_auto_20160826_2333'),
]
operations = [
migrations.AlterModelOptions(
name='inventory',
options={'ordering': ['-at']},
),
migrations.RemoveField(
model_name='supplierarticle',
name='box_capacity',
),
migrations.RemoveField(
model_name='supplierarticle',
name='box_type',
),
migrations.AddField(
model_name='article',
name='box_capacity',
field=models.PositiveSmallIntegerField(blank=True, null=True, default=None),
),
migrations.AddField(
model_name='article',
name='box_type',
field=models.CharField(max_length=7, blank=True, null=True, default=None, choices=[('caisse', 'Caisse'), ('carton', 'Carton'), ('palette', 'Palette'), ('fût', 'Fût')]),
),
]

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('kfet', '0038_auto_20160828_0402'),
]
operations = [
migrations.AlterField(
model_name='order',
name='amount',
field=models.DecimalField(default=0, decimal_places=2, max_digits=6),
),
migrations.AlterField(
model_name='orderarticle',
name='quantity_received',
field=models.IntegerField(default=0),
),
]

View file

@ -325,6 +325,18 @@ class Article(models.Model):
category = models.ForeignKey( category = models.ForeignKey(
ArticleCategory, on_delete = models.PROTECT, ArticleCategory, on_delete = models.PROTECT,
related_name = "articles") related_name = "articles")
BOX_TYPE_CHOICES = (
("caisse", "caisse"),
("carton", "carton"),
("palette", "palette"),
("fût", "fût"),
)
box_type = models.CharField(
choices = BOX_TYPE_CHOICES,
max_length = choices_length(BOX_TYPE_CHOICES),
blank = True, null = True, default = None)
box_capacity = models.PositiveSmallIntegerField(
blank = True, null = True, default = None)
def __str__(self): def __str__(self):
return '%s - %s' % (self.category.name, self.name) return '%s - %s' % (self.category.name, self.name)
@ -391,18 +403,6 @@ class SupplierArticle(models.Model):
Supplier, on_delete = models.PROTECT) Supplier, on_delete = models.PROTECT)
article = models.ForeignKey( article = models.ForeignKey(
Article, on_delete = models.PROTECT) Article, on_delete = models.PROTECT)
BOX_TYPE_CHOICES = (
("caisse", "Caisse"),
("carton", "Carton"),
("palette", "Palette"),
("fût", "Fût"),
)
box_type = models.CharField(
choices = BOX_TYPE_CHOICES,
max_length = choices_length(BOX_TYPE_CHOICES),
blank = True, null = True, default = None)
box_capacity = models.PositiveSmallIntegerField(
blank = True, null = True, default = None)
price_HT = models.DecimalField( price_HT = models.DecimalField(
max_digits = 7, decimal_places = 4, max_digits = 7, decimal_places = 4,
blank = True, null = True, default = None) blank = True, null = True, default = None)
@ -422,7 +422,11 @@ class Order(models.Model):
through = "OrderArticle", through = "OrderArticle",
related_name = "orders") related_name = "orders")
at = models.DateTimeField(auto_now_add = True) at = models.DateTimeField(auto_now_add = True)
amount = models.DecimalField(max_digits = 6, decimal_places = 2) amount = models.DecimalField(
max_digits = 6, decimal_places = 2, default = 0)
class Meta:
ordering = ['-at']
class OrderArticle(models.Model): class OrderArticle(models.Model):
order = models.ForeignKey( order = models.ForeignKey(
@ -430,7 +434,7 @@ class OrderArticle(models.Model):
article = models.ForeignKey( article = models.ForeignKey(
Article, on_delete = models.PROTECT) Article, on_delete = models.PROTECT)
quantity_ordered = models.IntegerField() quantity_ordered = models.IntegerField()
quantity_received = models.IntegerField() quantity_received = models.IntegerField(default = 0)
class TransferGroup(models.Model): class TransferGroup(models.Model):
at = models.DateTimeField(auto_now_add = True) at = models.DateTimeField(auto_now_add = True)

View file

@ -17,19 +17,13 @@
<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">
<div class="content-right-block">
<h2>Liste des commandes</h2>
<div>
<table>
</table>
</div>
</div>
<div class="content-right-block"> <div class="content-right-block">
<h2>Fournisseurs</h2> <h2>Fournisseurs</h2>
<div> <div>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<td></td>
<td></td> <td></td>
<td>Nom</td> <td>Nom</td>
<td>Mail</td> <td>Mail</td>
@ -39,6 +33,11 @@
<tbody> <tbody>
{% for supplier in suppliers %} {% for supplier in suppliers %}
<tr> <tr>
<td>
<a href="{% url 'kfet.order.new' supplier.pk %}" class="btn btn-primary">
Passer une commande
</a>
</td>
<td> <td>
<a href="{% url 'kfet.order.supplier.update' supplier.pk %}"> <a href="{% url 'kfet.order.supplier.update' supplier.pk %}">
<span class="glyphicon glyphicon-cog"></span> <span class="glyphicon glyphicon-cog"></span>
@ -55,6 +54,23 @@
</table> </table>
</div> </div>
</div> </div>
<div class="content-right-block">
<h2>Liste des commandes</h2>
<div>
<table>
{% for order in orders %}
<tr>
<td>
<a href="{% url 'kfet.order.read' order.pk %}">
{{ order.at }}
</a>
</td>
<td>{{ order.supplier }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,62 @@
{% extends 'kfet/base.html' %}
{% block title %}Nouvelle commande{% endblock %}
{% block content-header-title %}Nouvelle commande {{ supplier.name }}{% endblock %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<table>
<thead>
<tr>
<td rowspan="2">Article</td>
<td colspan="5">Ventes</td>
<td rowspan="2">V. moy.</td>
<td rowspan="2">E.T.</td>
<td rowspan="2">Prév.</td>
<td rowspan="2">Stock</td>
<td rowspan="2">Rec.</td>
<td rowspan="2">Com.</td>
</tr>
<tr>
<td>S1</td>
<td>S2</td>
<td>S3</td>
<td>S4</td>
<td>S5</td>
</tr>
</thead>
<tbody>
{% for form in formset %}
{% ifchanged form.category %}
<tr>
<td colspan="11">{{ form.category_name }}</td>
</tr>
{% endifchanged %}
<tr>
{{ form.article }}
<td>{{ form.name }}</td>
<td>{{ form.v_s1 }}</td>
<td>{{ form.v_s2 }}</td>
<td>{{ form.v_s3 }}</td>
<td>{{ form.v_s4 }}</td>
<td>{{ form.v_s5 }}</td>
<td>{{ form.v_moy }}</td>
<td>{{ form.v_et }}</td>
<td>{{ form.v_prev }}</td>
<td>{{ form.stock }}</td>
<td>{{ form.c_rec }}</td>
<td>{{ form.quantity_ordered }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not perms.kfet.add_order %}
<input type="password" name="KFETPASSWORD">
{% endif %}
{{ formset.management_form }}
<input type="submit" class="btn btn-primary btn-lg" value="Envoyer">
</form>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'kfet/base.html' %}
{% block title %}Commande #{{ order.pk }}{% endblock %}
{% block content-header-title %}Commande #{{ order.pk }}{% endblock %}
{% block content %}
<p>
Créée le {{ order.at }} pour {{ order.supplier.name }}
</p>
<textarea>{{ mail }}</textarea>
{% endblock %}

View file

@ -179,7 +179,12 @@ urlpatterns = [
url(r'^orders/$', url(r'^orders/$',
permission_required('kfet.is_team')(views.OrderList.as_view()), permission_required('kfet.is_team')(views.OrderList.as_view()),
name = 'kfet.order'), name = 'kfet.order'),
url(r'^orders/(?P<pk>\d+)',
permission_required('kfet.is_team')(views.OrderRead.as_view()),
name = 'kfet.order.read'),
url(r'^orders/suppliers/(?P<pk>\d+)/edit$', url(r'^orders/suppliers/(?P<pk>\d+)/edit$',
permission_required('kfet.is_team')(views.SupplierUpdate.as_view()), permission_required('kfet.is_team')(views.SupplierUpdate.as_view()),
name = 'kfet.order.supplier.update'), name = 'kfet.order.supplier.update'),
url(r'^orders/suppliers/(?P<pk>\d+)/new-order$', views.order_create,
name = 'kfet.order.new'),
] ]

View file

@ -12,19 +12,21 @@ from django.contrib.auth.models import User, Permission, Group
from django.http import HttpResponse, JsonResponse, Http404 from django.http import HttpResponse, JsonResponse, Http404
from django.forms import modelformset_factory, formset_factory from django.forms import modelformset_factory, formset_factory
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.db.models import F, Sum, Prefetch, Count from django.db.models import F, Sum, Prefetch, Count, Func
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from gestioncof.models import CofProfile, Clipper from gestioncof.models import CofProfile, Clipper
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative, from kfet.models import (Account, Checkout, Article, Settings, AccountNegative,
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
InventoryArticle, Order) InventoryArticle, Order, OrderArticle)
from kfet.forms import * from kfet.forms import *
from collections import defaultdict from collections import defaultdict
from kfet import consumers from kfet import consumers
import datetime from datetime import timedelta
import django_cas_ng import django_cas_ng
import heapq
import statistics
@login_required @login_required
def home(request): def home(request):
@ -1293,7 +1295,6 @@ def inventory_create(request):
.order_by('category__name', 'name') .order_by('category__name', 'name')
) )
initial = [] initial = []
data = []
for article in articles: for article in articles:
initial.append({ initial.append({
'article' : article.pk, 'article' : article.pk,
@ -1324,6 +1325,7 @@ def inventory_create(request):
if form.cleaned_data['stock_new'] is not None: if form.cleaned_data['stock_new'] is not None:
if not saved: if not saved:
inventory.save() inventory.save()
saved = True
article = articles.get(pk=form.cleaned_data['article'].pk) article = articles.get(pk=form.cleaned_data['article'].pk)
stock_old = article.stock stock_old = article.stock
@ -1356,13 +1358,182 @@ def inventory_create(request):
class OrderList(ListView): class OrderList(ListView):
model = Order model = Order
template_name = 'kfet/order.html' template_name = 'kfet/order.html'
context_object_name = 'inventories' context_object_name = 'orders'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrderList, self).get_context_data(**kwargs) context = super(OrderList, self).get_context_data(**kwargs)
context['suppliers'] = Supplier.objects.order_by('name') context['suppliers'] = Supplier.objects.order_by('name')
return context return context
@permission_required('kfet.is_team')
def order_create(request, pk):
supplier = Supplier.objects.get(pk=pk)
articles = (Article.objects
.filter(suppliers=supplier.pk)
.select_related('category')
.order_by('category__name', 'name'))
print(articles[0].suppliers.all())
initial = []
today = timezone.now().date()
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))
.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 }
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]
v_3max = heapq.nlargest(3, v_all)
v_moy = statistics.mean(v_3max)
v_et = statistics.pstdev(v_3max, v_moy)
v_prev = v_moy + v_et
c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
if article.box_capacity:
c_rec_temp = c_rec_tot / box_capacity
if c_rec_temp >= 10:
c_rec = round(c_rec_temp)
elif c_rec_temp > 5:
c_rec = 10
elif c_rec_temp > 2:
c_rec = 5
else:
c_rec = round(c_rec_temp)
initial.append({
'article': article.pk,
'name': article.name,
'category': article.category_id,
'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_moy': round(v_moy),
'v_et': round(v_et),
'v_prev': round(v_prev),
'c_rec': article.box_capacity and c_rec or round(c_rec_tot),
})
cls_formset = formset_factory(
form = OrderArticleForm,
extra = 0)
if request.POST:
formset = cls_formset(request.POST, initial=initial)
if not request.user.has_perm('kfet.add_order'):
messages.error(request, 'Permission refusée')
elif formset.is_valid():
order = Order()
order.supplier = supplier
saved = False
for form in formset:
if form.cleaned_data['quantity_ordered'] is not None:
if not saved:
order.save()
saved = True
article = articles.get(pk=form.cleaned_data['article'].pk)
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)
if saved:
messages.success(request, 'Commande créée')
return redirect('kfet.order.read', order.pk)
messages.warning(request, 'Rien commandé => Pas de commande')
else:
messages.error('Corrigez les erreurs')
else:
formset = cls_formset(initial=initial)
return render(request, 'kfet/order_create.html', {
'supplier': supplier,
'formset' : formset,
})
class OrderRead(DetailView):
model = Order
template_name = 'kfet/order_read.html'
context_object_name = 'order'
def get_context_data(self, **kwargs):
context = super(OrderRead, self).get_context_data(**kwargs)
orderarticles = (OrderArticle.objects
.select_related('article', 'article__category')
.filter(order=self.object)
.order_by('article__category__name', 'article__name')
)
mail = ("Bonjour,\n\nNous voudrions pour le ##DATE## à la K-Fêt de "
"l'ENS Ulm :")
category = 0
for orderarticle in orderarticles:
if category != orderarticle.article.category:
category = orderarticle.article.category
mail += '\n'
nb = orderarticle.quantity_ordered
box = ''
if orderarticle.article.box_capacity:
nb /= orderarticle.article.box_capacity
if nb >= 2:
box = ' %ss de' % orderarticle.article.box_type
else:
box = ' %s de' % orderarticle.article.box_type
name = orderarticle.article.name.capitalize()
mail += "\n- %s%s %s" % (round(nb), box, name)
mail += ("\n\nMerci d'appeler le numéro suivant lorsque les livreurs "
"sont là : ##TELEPHONE##\nCordialement,\n##PRENOM## ##NOM## "
", pour la K-Fêt de l'ENS Ulm")
context['mail'] = mail
return context
class SupplierUpdate(SuccessMessageMixin, UpdateView): class SupplierUpdate(SuccessMessageMixin, UpdateView):
model = Supplier model = Supplier
template_name = 'kfet/supplier_form.html' template_name = 'kfet/supplier_form.html'