diff --git a/kfet/forms.py b/kfet/forms.py index 67103de6..7acd0880 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -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 # ----- diff --git a/kfet/migrations/0052_category_addcost.py b/kfet/migrations/0052_category_addcost.py new file mode 100644 index 00000000..83346a1a --- /dev/null +++ b/kfet/migrations/0052_category_addcost.py @@ -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'), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 6ddac84d..cb8c324b 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -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) diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index 17c831df..123f4cfa 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -16,6 +16,9 @@ Nouvel article + + Catégories + diff --git a/kfet/templates/kfet/article_update.html b/kfet/templates/kfet/article_update.html index 85a29f6b..a3bfbcc6 100644 --- a/kfet/templates/kfet/article_update.html +++ b/kfet/templates/kfet/article_update.html @@ -12,7 +12,7 @@
-
+ {% csrf_token %} {% include 'kfet/form_snippet.html' with form=form %} {% if not perms.kfet.change_article %} diff --git a/kfet/templates/kfet/category.html b/kfet/templates/kfet/category.html new file mode 100644 index 00000000..5393bf59 --- /dev/null +++ b/kfet/templates/kfet/category.html @@ -0,0 +1,53 @@ +{% extends 'kfet/base.html' %} + +{% block title %}Categories d'articles{% endblock %} +{% block content-header-title %}Categories d'articles{% endblock %} + +{% block content %} + +
+
+
+
+
{{ categories|length }}
+
catégorie{{ categories|length|pluralize }}
+
+
+
+
+ {% include 'kfet/base_messages.html' %} +
+
+

Liste des catégories

+
+ + + + + + + + + + + {% for category in categories %} + + + + + + + {% endfor %} + +
NomNombre d'articlesPeut être majorée
+ + + + {{ category.name }}{{ category.articles.all|length }}{{ category.has_addcost | yesno:"Oui,Non"}}
+
+
+
+
+
+ +{% endblock %} diff --git a/kfet/templates/kfet/category_update.html b/kfet/templates/kfet/category_update.html new file mode 100644 index 00000000..1a26d001 --- /dev/null +++ b/kfet/templates/kfet/category_update.html @@ -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" %} + +
+
+
+ + {% 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"%} + +
+
+
+ +{% endblock %} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index f7246ec3..c9ee82ee 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -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.{3})/stat/operations/list$', + url(r'^accounts/(?P.{3})/stat/operations/list$', views.AccountStatOperationList.as_view(), name='kfet.account.stat.operation.list'), - url('^accounts/(?P.{3})/stat/operations$', + url(r'^accounts/(?P.{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\d+)/edit$', + teamkfet_required(views.CategoryUpdate.as_view()), + name='kfet.category.update'), # Article - General url('^articles/$', teamkfet_required(views.ArticleList.as_view()), diff --git a/kfet/views.py b/kfet/views.py index 524b35ba..de6c906e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -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) })