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,7 +831,10 @@ $(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'])
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'])

View file

@ -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
@ -723,12 +723,44 @@ 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')
@ -739,8 +771,8 @@ class ArticleList(ListView):
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'
@ -784,8 +816,8 @@ class ArticleCreate(SuccessMessageMixin, CreateView):
# Creating
return super(ArticleCreate, self).form_valid(form)
# Article - Read
# Article - Read
class ArticleRead(DetailView):
model = Article
template_name = 'kfet/article_read.html'
@ -805,8 +837,8 @@ class ArticleRead(DetailView):
context['supplierarts'] = supplierarts
return context
# Article - Update
# Article - Update
class ArticleUpdate(SuccessMessageMixin, UpdateView):
model = Article
template_name = 'kfet/article_update.html'
@ -930,7 +962,8 @@ def kpsul_update_addcost(request):
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,15 +984,19 @@ 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)]
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))
.values_list('name', flat=True)
)
return missing_perms
@teamkfet_required
def kpsul_perform_operations(request):
# Initializing response data
@ -995,24 +1033,26 @@ def kpsul_perform_operations(request):
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)
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:
if is_addcost & operation.article.category.has_addcost:
operation.addcost_for = addcost_for
operation.addcost_amount = addcost_amount * operation.article_nb
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(
(perms, stop) = (operationgroup.on_acc
.perms_to_perform_operation(
amount=operationgroup.amount)
)
required_perms |= perms
if need_comment:
@ -1072,8 +1114,8 @@ def kpsul_perform_operations(request):
negative = AccountNegative(
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
@ -1118,10 +1160,13 @@ def kpsul_perform_operations(request):
}]
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,
@ -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) })