Merge branch 'qwann/k-fet/category_addcost' into 'master'

K-Fêt - Majorations
- Seulement les catégories préalablement sélectionnées sont majorées le 
le cas échéant.
- Pour modifier cette sélection, suivre le lien "Catégories" depuis la
liste des articles.

Fixes #149

See merge request !189
This commit is contained in:
Aurélien Delobelle 2017-04-05 15:52:15 +02:00
commit ebf948d042
10 changed files with 265 additions and 85 deletions

View file

@ -233,6 +233,16 @@ class CheckoutStatementUpdateForm(forms.ModelForm):
model = CheckoutStatement
exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken']
# -----
# Category
# -----
class CategoryForm(forms.ModelForm):
class Meta:
model = ArticleCategory
fields = ['name', 'has_addcost']
# -----
# Article forms
# -----

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0051_verbose_names'),
]
operations = [
migrations.AddField(
model_name='articlecategory',
name='has_addcost',
field=models.BooleanField(default=True, help_text="Si oui et qu'une majoration est active, celle-ci sera appliquée aux articles de cette catégorie.", verbose_name='majorée'),
),
migrations.AlterField(
model_name='articlecategory',
name='name',
field=models.CharField(max_length=45, verbose_name='nom'),
),
]

View file

@ -338,13 +338,20 @@ class CheckoutStatement(models.Model):
balance=F('balance') - last_statement.balance_new + self.balance_new)
super(CheckoutStatement, self).save(*args, **kwargs)
@python_2_unicode_compatible
class ArticleCategory(models.Model):
name = models.CharField(max_length = 45)
name = models.CharField("nom", max_length=45)
has_addcost = models.BooleanField("majorée", default=True,
help_text="Si oui et qu'une majoration "
"est active, celle-ci sera "
"appliquée aux articles de "
"cette catégorie.")
def __str__(self):
return self.name
@python_2_unicode_compatible
class Article(models.Model):
name = models.CharField("nom", max_length = 45)

View file

@ -16,6 +16,9 @@
<a class="btn btn-primary btn-lg" href="{% url 'kfet.article.create' %}">
Nouvel article
</a>
<a class="btn btn-primary btn-lg" href="{% url 'kfet.category' %}">
Catégories
</a>
</div>
</div>
</div>

View file

@ -12,7 +12,7 @@
<div class="row form-only">
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_article %}

View file

@ -0,0 +1,53 @@
{% extends 'kfet/base.html' %}
{% block title %}Categories d'articles{% endblock %}
{% block content-header-title %}Categories d'articles{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-4 col-md-3 col-content-left">
<div class="content-left">
<div class="content-left-top">
<div class="line line-big">{{ categories|length }}</div>
<div class="line line-bigsub">catégorie{{ categories|length|pluralize }}</div>
</div>
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
<div class="content-right-block">
<h2>Liste des catégories</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td></td>
<td>Nom</td>
<td class="text-right">Nombre d'articles</td>
<td class="text-right">Peut être majorée</td>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td class="text-center">
<a href="{% url 'kfet.category.update' category.pk %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</td>
<td>{{ category.name }}</td>
<td class="text-right">{{ category.articles.all|length }}</td>
<td class="text-right">{{ category.has_addcost | yesno:"Oui,Non"}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends 'kfet/base.html' %}
{% block title %}Édition de la catégorie {{ category.name }}{% endblock %}
{% block content-header-title %}Catégorie {{ category.name }} - Édition{% endblock %}
{% block content %}
{% include "kfet/base_messages.html" %}
<div class="row form-only">
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.edit_articlecategory %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer"%}
<form>
</div>
</div>
</div>
{% endblock %}

View file

@ -647,7 +647,7 @@ $(document).ready(function() {
});
$after.after(article_html);
// Pour l'autocomplétion
articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock']]);
articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock'],article['category__has_addcost']]);
}
function getArticles() {
@ -831,8 +831,11 @@ $(document).ready(function() {
while (i<articlesList.length && id != articlesList[i][1]) i++;
article_data = articlesList[i];
var amount_euro = - article_data[3] * nb ;
if (settings['addcost_for'] && settings['addcost_amount'] && account_data['trigramme'] != settings['addcost_for'])
amount_euro -= settings['addcost_amount'] * nb;
if (settings['addcost_for']
&& settings['addcost_amount']
&& account_data['trigramme'] != settings['addcost_for']
&& article_data[5])
amount_euro -= settings['addcost_amount'] * nb;
var reduc_divisor = 1;
if (account_data['is_cof'])
reduc_divisor = 1 + settings['subvention_cof'] / 100;

View file

@ -8,7 +8,7 @@ from kfet.decorators import teamkfet_required
urlpatterns = [
url(r'^$', views.Home.as_view(),
name = 'kfet.home'),
name='kfet.home'),
url(r'^login/genericteam$', views.login_genericteam,
name='kfet.login.genericteam'),
url(r'^history$', views.history,
@ -69,10 +69,10 @@ urlpatterns = [
name='kfet.account.negative'),
# Account - Statistics
url('^accounts/(?P<trigramme>.{3})/stat/operations/list$',
url(r'^accounts/(?P<trigramme>.{3})/stat/operations/list$',
views.AccountStatOperationList.as_view(),
name='kfet.account.stat.operation.list'),
url('^accounts/(?P<trigramme>.{3})/stat/operations$',
url(r'^accounts/(?P<trigramme>.{3})/stat/operations$',
views.AccountStatOperation.as_view(),
name='kfet.account.stat.operation'),
@ -125,6 +125,14 @@ urlpatterns = [
# Article urls
# -----
# Category - General
url('^categories/$',
teamkfet_required(views.CategoryList.as_view()),
name='kfet.category'),
# Category - Update
url('^categories/(?P<pk>\d+)/edit$',
teamkfet_required(views.CategoryUpdate.as_view()),
name='kfet.category.update'),
# Article - General
url('^articles/$',
teamkfet_required(views.ArticleList.as_view()),

View file

@ -29,7 +29,7 @@ from kfet.models import (
Account, Checkout, Article, Settings, AccountNegative,
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
TransferGroup, Transfer)
TransferGroup, Transfer, ArticleCategory)
from kfet.forms import (
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
@ -39,7 +39,7 @@ from kfet.forms import (
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
KPsulOperationFormSet, AddcostForm, FilterHistoryForm, SettingsForm,
TransferFormSet, InventoryArticleForm, OrderArticleForm,
OrderArticleToInventoryForm
OrderArticleToInventoryForm, CategoryForm
)
from collections import defaultdict
from kfet import consumers
@ -52,7 +52,7 @@ from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes
class Home(TemplateView):
template_name = "kfet/home.html"
template_name = "kfet/home.html"
def get_context_data(self, **kwargs):
context = super(TemplateView, self).get_context_data(**kwargs)
@ -723,28 +723,60 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
form.instance.amount_taken = getAmountTaken(form.instance)
return super(CheckoutStatementUpdate, self).form_valid(form)
# -----
# Category views
# -----
# Category - General
class CategoryList(ListView):
queryset = (ArticleCategory.objects
.prefetch_related('articles')
.order_by('name'))
template_name = 'kfet/category.html'
context_object_name = 'categories'
# Category - Update
class CategoryUpdate(SuccessMessageMixin, UpdateView):
model = ArticleCategory
template_name = 'kfet/category_update.html'
form_class = CategoryForm
success_url = reverse_lazy('kfet.category')
success_message = "Informations mises à jour pour la catégorie : %(name)s"
# Surcharge de la validation
def form_valid(self, form):
# Checking permission
if not self.request.user.has_perm('kfet.change_articlecategory'):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
return super(CategoryUpdate, self).form_valid(form)
# -----
# Article views
# -----
# Article - General
# 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'))
.select_related('category')
.prefetch_related(Prefetch('inventories',
queryset=Inventory.objects.order_by('-at'),
to_attr='inventory'))
.order_by('category', '-is_sold', 'name'))
template_name = 'kfet/article.html'
context_object_name = 'articles'
# Article - Create
# Article - Create
class ArticleCreate(SuccessMessageMixin, CreateView):
model = Article
template_name = 'kfet/article_create.html'
form_class = ArticleForm
model = Article
template_name = 'kfet/article_create.html'
form_class = ArticleForm
success_message = 'Nouvel item : %(category)s - %(name)s'
# Surcharge de la validation
@ -759,7 +791,7 @@ class ArticleCreate(SuccessMessageMixin, CreateView):
# Save des suppliers déjà existant
for supplier in form.cleaned_data['suppliers']:
SupplierArticle.objects.create(
article = article, supplier = supplier)
article=article, supplier=supplier)
# Nouveau supplier
supplier_new = form.cleaned_data['supplier_new'].strip()
@ -768,49 +800,49 @@ class ArticleCreate(SuccessMessageMixin, CreateView):
name=supplier_new)
if created:
SupplierArticle.objects.create(
article = article, supplier = supplier)
article=article, supplier=supplier)
# Inventaire avec stock initial
inventory = Inventory()
inventory.by = self.request.user.profile.account_kfet
inventory.save()
InventoryArticle.objects.create(
inventory = inventory,
article = article,
stock_old = article.stock,
stock_new = article.stock,
inventory=inventory,
article=article,
stock_old=article.stock,
stock_new=article.stock,
)
# Creating
return super(ArticleCreate, self).form_valid(form)
# Article - Read
# Article - Read
class ArticleRead(DetailView):
model = Article
model = Article
template_name = 'kfet/article_read.html'
context_object_name = 'article'
def get_context_data(self, **kwargs):
context = super(ArticleRead, self).get_context_data(**kwargs)
inventoryarts = (InventoryArticle.objects
.filter(article = self.object)
.select_related('inventory')
.order_by('-inventory__at'))
.filter(article=self.object)
.select_related('inventory')
.order_by('-inventory__at'))
context['inventoryarts'] = inventoryarts
supplierarts = (SupplierArticle.objects
.filter(article = self.object)
.select_related('supplier')
.order_by('-at'))
.filter(article=self.object)
.select_related('supplier')
.order_by('-at'))
context['supplierarts'] = supplierarts
return context
# Article - Update
# Article - Update
class ArticleUpdate(SuccessMessageMixin, UpdateView):
model = Article
template_name = 'kfet/article_update.html'
form_class = ArticleRestrictForm
model = Article
template_name = 'kfet/article_update.html'
form_class = ArticleRestrictForm
success_message = "Informations mises à jour pour l'article : %(name)s"
# Surcharge de la validation
@ -826,13 +858,13 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
for supplier in form.cleaned_data['suppliers']:
if supplier not in article.suppliers.all():
SupplierArticle.objects.create(
article = article, supplier = supplier)
article=article, supplier=supplier)
# On vire les suppliers désélectionnés
for supplier in article.suppliers.all():
if supplier not in form.cleaned_data['suppliers']:
SupplierArticle.objects.filter(
article = article, supplier = supplier).delete()
article=article, supplier=supplier).delete()
# Nouveau supplier
supplier_new = form.cleaned_data['supplier_new'].strip()
@ -841,7 +873,7 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
name=supplier_new)
if created:
SupplierArticle.objects.create(
article = article, supplier = supplier)
article=article, supplier=supplier)
# Updating
return super(ArticleUpdate, self).form_valid(form)
@ -924,13 +956,14 @@ def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST)
if not addcost_form.is_valid():
data = { 'errors': { 'addcost': list(addcost_form.errors) } }
data = {'errors': {'addcost': list(addcost_form.errors)}}
return JsonResponse(data, status=400)
required_perms = ['kfet.manage_addcosts']
if not request.user.has_perms(required_perms):
data = {
'errors': {
'missing_perms': get_missing_perms(required_perms, request.user)
'missing_perms': get_missing_perms(required_perms,
request.user)
}
}
return JsonResponse(data, status=403)
@ -938,7 +971,8 @@ def kpsul_update_addcost(request):
trigramme = addcost_form.cleaned_data['trigramme']
account = trigramme and Account.objects.get(trigramme=trigramme) or None
Settings.objects.filter(name='ADDCOST_FOR').update(value_account=account)
Settings.objects.filter(name='ADDCOST_AMOUNT').update(value_decimal=addcost_form.cleaned_data['amount'])
(Settings.objects.filter(name='ADDCOST_AMOUNT')
.update(value_decimal=addcost_form.cleaned_data['amount']))
cache.delete('ADDCOST_FOR')
cache.delete('ADDCOST_AMOUNT')
data = {
@ -950,20 +984,24 @@ def kpsul_update_addcost(request):
consumers.KPsul.group_send('kfet.kpsul', data)
return JsonResponse(data)
def get_missing_perms(required_perms, user):
missing_perms_codenames = [ (perm.split('.'))[1]
for perm in required_perms if not user.has_perm(perm)]
missing_perms_codenames = [(perm.split('.'))[1]
for perm in required_perms
if not user.has_perm(perm)]
missing_perms = list(
Permission.objects
.filter(codename__in=missing_perms_codenames)
.values_list('name', flat=True))
.filter(codename__in=missing_perms_codenames)
.values_list('name', flat=True)
)
return missing_perms
@teamkfet_required
def kpsul_perform_operations(request):
# Initializing response data
data = { 'operationgroup': 0, 'operations': [],
'warnings': {}, 'errors': {} }
data = {'operationgroup': 0, 'operations': [],
'warnings': {}, 'errors': {}}
# Checking operationgroup
operationgroup_form = KPsulOperationGroupForm(request.POST)
@ -971,7 +1009,7 @@ def kpsul_perform_operations(request):
data['errors']['operation_group'] = list(operationgroup_form.errors)
# Checking operation_formset
operation_formset = KPsulOperationFormSet(request.POST)
operation_formset = KPsulOperationFormSet(request.POST)
if not operation_formset.is_valid():
data['errors']['operations'] = list(operation_formset.errors)
@ -980,39 +1018,41 @@ def kpsul_perform_operations(request):
return JsonResponse(data, status=400)
# Pre-saving (no commit)
operationgroup = operationgroup_form.save(commit = False)
operations = operation_formset.save(commit = False)
operationgroup = operationgroup_form.save(commit=False)
operations = operation_formset.save(commit=False)
# Retrieving COF grant
cof_grant = Settings.SUBVENTION_COF()
# Retrieving addcosts data
addcost_amount = Settings.ADDCOST_AMOUNT()
addcost_for = Settings.ADDCOST_FOR()
addcost_for = Settings.ADDCOST_FOR()
# Initializing vars
required_perms = set() # Required perms to perform all operations
required_perms = set() # Required perms to perform all operations
cof_grant_divisor = 1 + cof_grant / 100
to_addcost_for_balance = 0 # For balance of addcost_for
to_checkout_balance = 0 # For balance of selected checkout
to_articles_stocks = defaultdict(lambda:0) # For stocks articles
is_addcost = (addcost_for and addcost_amount
and addcost_for != operationgroup.on_acc)
to_addcost_for_balance = 0 # For balance of addcost_for
to_checkout_balance = 0 # For balance of selected checkout
to_articles_stocks = defaultdict(lambda: 0) # For stocks articles
is_addcost = all((addcost_for, addcost_amount,
addcost_for != operationgroup.on_acc))
need_comment = operationgroup.on_acc.need_comment
# Filling data of each operations + operationgroup + calculating other stuffs
# Filling data of each operations
# + operationgroup + calculating other stuffs
for operation in operations:
if operation.type == Operation.PURCHASE:
operation.amount = - operation.article.price * operation.article_nb
if is_addcost:
operation.addcost_for = addcost_for
operation.addcost_amount = addcost_amount * operation.article_nb
operation.amount -= operation.addcost_amount
to_addcost_for_balance += operation.addcost_amount
if is_addcost & operation.article.category.has_addcost:
operation.addcost_for = addcost_for
operation.addcost_amount = addcost_amount \
* operation.article_nb
operation.amount -= operation.addcost_amount
to_addcost_for_balance += operation.addcost_amount
if operationgroup.on_acc.is_cash:
to_checkout_balance += -operation.amount
if operationgroup.on_acc.is_cof:
if is_addcost:
operation.addcost_amount = operation.addcost_amount / cof_grant_divisor
if is_addcost and operation.article.category.has_addcost:
operation.addcost_amount /= cof_grant_divisor
operation.amount = operation.amount / cof_grant_divisor
to_articles_stocks[operation.article] -= operation.article_nb
else:
@ -1029,8 +1069,10 @@ def kpsul_perform_operations(request):
if operationgroup.on_acc.is_cof:
to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
(perms, stop) = operationgroup.on_acc.perms_to_perform_operation(
amount = operationgroup.amount)
(perms, stop) = (operationgroup.on_acc
.perms_to_perform_operation(
amount=operationgroup.amount)
)
required_perms |= perms
if need_comment:
@ -1061,7 +1103,7 @@ def kpsul_perform_operations(request):
# saving account's balance and adding to Negative if not in
if not operationgroup.on_acc.is_cash:
Account.objects.filter(pk=operationgroup.on_acc.pk).update(
balance = F('balance') + operationgroup.amount)
balance=F('balance') + operationgroup.amount)
operationgroup.on_acc.refresh_from_db()
if operationgroup.on_acc.balance < 0:
if hasattr(operationgroup.on_acc, 'negative'):
@ -1070,21 +1112,21 @@ def kpsul_perform_operations(request):
operationgroup.on_acc.negative.save()
else:
negative = AccountNegative(
account = operationgroup.on_acc, start = timezone.now())
account=operationgroup.on_acc, start=timezone.now())
negative.save()
elif (hasattr(operationgroup.on_acc, 'negative')
and not operationgroup.on_acc.negative.balance_offset):
elif (hasattr(operationgroup.on_acc, 'negative') and
not operationgroup.on_acc.negative.balance_offset):
operationgroup.on_acc.negative.delete()
# Updating checkout's balance
if to_checkout_balance:
Checkout.objects.filter(pk=operationgroup.checkout.pk).update(
balance = F('balance') + to_checkout_balance)
balance=F('balance') + to_checkout_balance)
# Saving addcost_for with new balance if there is one
if is_addcost and to_addcost_for_balance:
Account.objects.filter(pk=addcost_for.pk).update(
balance = F('balance') + to_addcost_for_balance)
balance=F('balance') + to_addcost_for_balance)
# Saving operation group
operationgroup.save()
@ -1099,7 +1141,7 @@ def kpsul_perform_operations(request):
# Updating articles stock
for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update(
stock = F('stock') + to_articles_stocks[article])
stock=F('stock') + to_articles_stocks[article])
# Websocket data
websocket_data = {}
@ -1111,17 +1153,20 @@ def kpsul_perform_operations(request):
'at': operationgroup.at,
'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment,
'valid_by__trigramme': ( operationgroup.valid_by and
operationgroup.valid_by.trigramme or None),
'valid_by__trigramme': (operationgroup.valid_by and
operationgroup.valid_by.trigramme or None),
'on_acc__trigramme': operationgroup.on_acc.trigramme,
'opes': [],
}]
for operation in operations:
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_for__trigramme': operation.addcost_for and addcost_for.trigramme or None,
'article__name': operation.article and operation.article.name or None,
'addcost_for__trigramme': (
operation.addcost_for and addcost_for.trigramme or None),
'article__name': (
operation.article and operation.article.name or None),
'article_nb': operation.article_nb,
'group_id': operationgroup.pk,
'canceled_by__trigramme': None, 'canceled_at': None,
@ -1135,7 +1180,7 @@ def kpsul_perform_operations(request):
}]
websocket_data['articles'] = []
# Need refresh from db cause we used update on querysets
articles_pk = [ article.pk for article in to_articles_stocks]
articles_pk = [article.pk for article in to_articles_stocks]
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
for article in articles:
websocket_data['articles'].append({
@ -1145,6 +1190,7 @@ def kpsul_perform_operations(request):
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
return JsonResponse(data)
@teamkfet_required
def kpsul_cancel_operations(request):
# Pour la réponse
@ -1393,7 +1439,8 @@ def history_json(request):
def kpsul_articles_data(request):
articles = (
Article.objects
.values('id', 'name', 'price', 'stock', 'category_id', 'category__name')
.values('id', 'name', 'price', 'stock', 'category_id',
'category__name', 'category__has_addcost')
.filter(is_sold=True))
return JsonResponse({ 'articles': list(articles) })