diff --git a/gestion/admin.py b/gestion/admin.py index f2deb46..aa5508c 100644 --- a/gestion/admin.py +++ b/gestion/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from simple_history.admin import SimpleHistoryAdmin -from .models import Reload, Refund, Product, Keg, ConsumptionHistory, KegHistory, Consumption, Menu, MenuHistory +from .models import Reload, Refund, Product, Keg, ConsumptionHistory, KegHistory, Consumption, Menu, MenuHistory, Category class ConsumptionAdmin(SimpleHistoryAdmin): """ @@ -81,6 +81,12 @@ class RefundAdmin(SimpleHistoryAdmin): ordering = ('-date', 'amount', 'customer') search_fields = ('customer',) +class CategoryAdmin(SimpleHistoryAdmin): + """ + The admin class for Category + """ + ordering = ("order",) + admin.site.register(Reload, ReloadAdmin) admin.site.register(Refund, RefundAdmin) admin.site.register(Product, ProductAdmin) @@ -89,4 +95,5 @@ admin.site.register(ConsumptionHistory, ConsumptionHistoryAdmin) admin.site.register(KegHistory, KegHistoryAdmin) admin.site.register(Consumption, ConsumptionAdmin) admin.site.register(Menu, MenuAdmin) -admin.site.register(MenuHistory, MenuHistoryAdmin) \ No newline at end of file +admin.site.register(MenuHistory, MenuHistoryAdmin) +admin.site.register(Category, CategoryAdmin) \ No newline at end of file diff --git a/gestion/forms.py b/gestion/forms.py index 9e734a0..535a238 100644 --- a/gestion/forms.py +++ b/gestion/forms.py @@ -4,7 +4,7 @@ from django.contrib.auth.models import User from dal import autocomplete -from .models import Reload, Refund, Product, Keg, Menu +from .models import Reload, Refund, Product, Keg, Menu, Category from preferences.models import PaymentMethod class ReloadForm(forms.ModelForm): @@ -108,4 +108,18 @@ class GenerateReleveForm(forms.Form): A form to generate a releve. """ begin = forms.DateTimeField(label="Date de début") - end = forms.DateTimeField(label="Date de fin") \ No newline at end of file + end = forms.DateTimeField(label="Date de fin") + +class CategoryForm(forms.ModelForm): + """ + A form to create and edit a :class:`~gestion.models.Category`. + """ + class Meta: + model = Category + fields = "__all__" + +class SearchCategoryForm(forms.Form): + """ + A form to search a :class:`~gestion.models.Category`. + """ + category = forms.ModelChoiceField(queryset=Category.objects.all(), required=True, label="Catégorie", widget=autocomplete.ModelSelect2(url='gestion:categories-autocomplete', attrs={'data-minimum-input-length':2})) \ No newline at end of file diff --git a/gestion/migrations/0007_auto_20190503_1841.py b/gestion/migrations/0007_auto_20190503_1841.py new file mode 100644 index 0000000..fb1b9bb --- /dev/null +++ b/gestion/migrations/0007_auto_20190503_1841.py @@ -0,0 +1,46 @@ +# Generated by Django 2.1 on 2019-05-03 16:41 + +from django.db import migrations, models +import django.db.models.deletion + +def create_default_category(apps, schema_editor): + Category = apps.get_model('gestion', 'Category') + new_category = Category(name="Autre") + new_category.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('gestion', '0006_auto_20190227_0859'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='Nom')), + ], + ), + migrations.RemoveField( + model_name='historicalproduct', + name='category', + ), + migrations.RemoveField( + model_name='product', + name='category', + ), + migrations.RunPython(create_default_category), + migrations.AddField( + model_name='historicalproduct', + name='category', + field=models.ForeignKey(default=1, blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='gestion.Category'), + preserve_default=False, + ), + migrations.AddField( + model_name='product', + name='category', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='gestion.Category'), + preserve_default=False, + ), + ] diff --git a/gestion/migrations/0008_auto_20190503_1908.py b/gestion/migrations/0008_auto_20190503_1908.py new file mode 100644 index 0000000..193a342 --- /dev/null +++ b/gestion/migrations/0008_auto_20190503_1908.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1 on 2019-05-03 17:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gestion', '0007_auto_20190503_1841'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'verbose_name': 'Catégorie'}, + ), + migrations.AddField( + model_name='category', + name='order', + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name='product', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='gestion.Category', verbose_name='Catégorie'), + ), + ] diff --git a/gestion/models.py b/gestion/models.py index 116a85e..d00a2f1 100644 --- a/gestion/models.py +++ b/gestion/models.py @@ -5,27 +5,32 @@ from django.core.validators import MinValueValidator from preferences.models import PaymentMethod from django.core.exceptions import ValidationError +class Category(models.Model): + """ + A product category + """ + class Meta: + verbose_name="Catégorie" + name = models.CharField(max_length=100, verbose_name="Nom", unique=True) + order = models.IntegerField(default=0) + """ + The name of the category + """ + + def __str__(self): + return self.name + + @property + def active_products(self): + """ + Return active producs of this category + """ + return self.product_set.filter(is_active=True) class Product(models.Model): """ Stores a product. """ - P_PRESSION = 'PP' - D_PRESSION = 'DP' - G_PRESSION = 'GP' - BOTTLE = 'BT' - SOFT = 'SO' - FOOD = 'FO' - PANINI = 'PA' - TYPEINPUT_CHOICES_CATEGORIE = ( - (P_PRESSION, "Pinte Pression"), - (D_PRESSION, "Demi Pression"), - (G_PRESSION, "Galopin pression"), - (BOTTLE, "Bouteille"), - (SOFT, "Soft"), - (FOOD, "En-cas"), - (PANINI, "Ingredients panini"), - ) class Meta: verbose_name = "Produit" name = models.CharField(max_length=40, verbose_name="Nom", unique=True) @@ -48,7 +53,7 @@ class Product(models.Model): """ The barcode of the product. """ - category = models.CharField(max_length=2, choices=TYPEINPUT_CHOICES_CATEGORIE, default=FOOD, verbose_name="Catégorie") + category = models.ForeignKey('Category', on_delete=models.PROTECT, verbose_name="Catégorie") """ The category of the product """ diff --git a/gestion/templates/gestion/categories_list.html b/gestion/templates/gestion/categories_list.html new file mode 100644 index 0000000..10bd2bd --- /dev/null +++ b/gestion/templates/gestion/categories_list.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} +{% block entete %}Gestion des produits{% endblock %} +{% block navbar%} + +{% endblock %} +{% block content %} +
+
+

Liste des catégories

+
+ {% if perms.gestion.add_category %} + Créer une catégorie

+ {% endif %} +
+ + + + + + + + + + {% for category in categories %} + + + + + + {% endfor %} + +
NomOrdreAdministrer
{{ category }}{% if category.order == 0 %}0 (non affichéé){% else %}{{category.order}}{% endif %} Profil {% if perms.gestion.change_category %} Modifier{% endif %}
+
+
+{% endblock %} diff --git a/gestion/templates/gestion/category_profile.html b/gestion/templates/gestion/category_profile.html new file mode 100644 index 0000000..b64217e --- /dev/null +++ b/gestion/templates/gestion/category_profile.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block entete %}Gestion des produits : Profil de {{category}}{% endblock %} +{% block navbar %} + +{% endblock %} +{% block content %} +
+
+

Général

+
+ Liste des catégories

+ {% if perms.gestion.change_category %}Modifier
{% endif %}
+ Nom : {{ category.name }}
+ Ordre : {{ category.order }}
+
+
+
+

Liste des produits ({{category.product_set.all.count}} au total dont {{category.active_products.count}} actifs)

+
+ +
+{% endblock %} diff --git a/gestion/templates/gestion/manage.html b/gestion/templates/gestion/manage.html index 3b4c3f0..8f0c22b 100644 --- a/gestion/templates/gestion/manage.html +++ b/gestion/templates/gestion/manage.html @@ -125,7 +125,7 @@ {% endif %} {% endfor %} - {% if not bieresPression|divisibleby:4 %} + {% if not cotisations|divisibleby:4 %} {% endif %} Bières pression @@ -141,8 +141,9 @@ {% if not bieresPression|divisibleby:4 %} {% endif %} - Bières bouteilles - {% for product in bieresBouteille %} + {% for category in categories %} + {{category}} + {% for product in category.active_products %} {% if forloop.counter0|divisibleby:4 %} {% endif %} @@ -151,49 +152,10 @@ {% endif %} {% endfor %} - {% if not bieresBouteille|divisibleby:4 %} - - {% endif %} - Paninis - {% for product in panini %} - {% if forloop.counter0|divisibleby:4 %} - - {% endif %} - - {% if forloop.counter|divisibleby:4 %} + {% if not category.active_products|divisibleby:4 %} {% endif %} {% endfor %} - {% if not panini|divisibleby:4 %} - - {% endif %} - Boissons sans alcool - {% for product in soft %} - {% if forloop.counter0|divisibleby:4 %} - - {% endif %} - - {% if forloop.counter|divisibleby:4 %} - - {% endif %} - {% endfor %} - {% if not soft|divisibleby:4 %} - - {% endif %} - - En-cas - {% for product in food %} - {% if forloop.counter0|divisibleby:4 %} - - {% endif %} - - {% if forloop.counter|divisibleby:4 %} - - {% endif %} - {% endfor %} - {% if not autreBouffe|divisibleby:4 %} - - {% endif %} {% if menus %} Menus {% for product in menus %} diff --git a/gestion/templates/gestion/product_profile.html b/gestion/templates/gestion/product_profile.html index 7035f2f..6c52477 100644 --- a/gestion/templates/gestion/product_profile.html +++ b/gestion/templates/gestion/product_profile.html @@ -17,7 +17,7 @@ Stock en soute : {{ product.stockHold }}
Stock au bar : {{ product.stockBar }}
Code Barre : {{ product.barcode }}
- Catégorie : {{ product.category }}
+ Catégorie : {{ product.category }}
Actif : {{ product.is_active | yesno:"Oui, Non"}}
Dégré : {{ product.deg }}
Volume : {{ product.volume }}cl
diff --git a/gestion/templates/gestion/products_index.html b/gestion/templates/gestion/products_index.html index 764c143..545bd6e 100644 --- a/gestion/templates/gestion/products_index.html +++ b/gestion/templates/gestion/products_index.html @@ -2,6 +2,9 @@ {% block entete %}Gestion des produits{% endblock %} {% block navbar%} {% endblock %} {% block content %} +{% if perms.gestion.add_category or perms.gestion.view_category %} +
+
+

Catégories

+
+ Actions possibles : + +
+{% endif %} {% if perms.gestion.add_product or perms.gestion.view_product %}
diff --git a/gestion/urls.py b/gestion/urls.py index 538a7d4..0b68c60 100644 --- a/gestion/urls.py +++ b/gestion/urls.py @@ -45,4 +45,11 @@ urlpatterns = [ path('menus-autcomplete', views.MenusAutocomplete.as_view(), name="menus-autocomplete"), path('cancelReload/', views.cancel_reload, name="cancelReload"), path('gen_releve', views.gen_releve, name="gen_releve"), + path('categoriesList', views.categoriesList, name="catrgorisList"), + path('addCategory', views.addCategory, name="addCategory"), + path('editCategory/', views.editCategory, name="editCategory"), + path('searchCategory', views.searchCategory, name="searchCategory"), + path('categoryProfile/', views.categoryProfile, name="categoryProfile"), + path('categoriesList', views.categoriesList, name="categoriesList"), + path('categories-autocomplete', views.CategoriesAutocomplete.as_view(), name="categories-autocomplete"), ] \ No newline at end of file diff --git a/gestion/views.py b/gestion/views.py index 3008704..07a459f 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -18,8 +18,8 @@ import simplejson as json from dal import autocomplete from decimal import * -from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm, SelectPositiveKegForm, SelectActiveKegForm, PinteForm, GenerateReleveForm -from .models import Product, Menu, Keg, ConsumptionHistory, KegHistory, Consumption, MenuHistory, Pinte, Reload, Refund +from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm, SelectPositiveKegForm, SelectActiveKegForm, PinteForm, GenerateReleveForm, CategoryForm, SearchCategoryForm +from .models import Product, Menu, Keg, ConsumptionHistory, KegHistory, Consumption, MenuHistory, Pinte, Reload, Refund, Category from preferences.models import PaymentMethod, GeneralPreferences, Cotisation from users.models import CotisationHistory @@ -30,15 +30,12 @@ def manage(request): """ Displays the manage view. """ + categories = Category.objects.exclude(order=0).order_by('order') pay_buttons = PaymentMethod.objects.filter(is_active=True) gestion_form = GestionForm(request.POST or None) reload_form = ReloadForm(request.POST or None) refund_form = RefundForm(request.POST or None) bieresPression = [] - bieresBouteille = Product.objects.filter(category=Product.BOTTLE).filter(is_active=True) - panini = Product.objects.filter(category=Product.PANINI).filter(is_active=True) - food = Product.objects.filter(category=Product.FOOD).filter(is_active=True) - soft = Product.objects.filter(category=Product.SOFT).filter(is_active=True) menus = Menu.objects.filter(is_active=True) kegs = Keg.objects.filter(is_active=True) gp, _ = GeneralPreferences.objects.get_or_create(pk=1) @@ -56,11 +53,7 @@ def manage(request): "reload_form": reload_form, "refund_form": refund_form, "bieresPression": bieresPression, - "bieresBouteille": bieresBouteille, - "panini": panini, - "food": food, - "soft": soft, - "menus": menus, + "categories": categories, "pay_buttons": pay_buttons, "floating_buttons": floating_buttons, "cotisations": cotisations @@ -884,3 +877,82 @@ def gen_releve(request): return render_to_pdf(request, 'gestion/releve.tex', {"consumptions": consumptions, "reloads": reloads, "refunds": refunds, "cotisations": cotisations, "begin": begin, "end": end, "now": now, "value_especes": value_especes, "value_lydia": value_lydia, "value_cheque": value_cheque}, filename="releve.pdf") else: return render(request, "form.html", {"form": form, "form_title": "Génération d'un relevé", "form_button": "Générer", "form_button_icon": "file-pdf"}) + + +########## categories ########## +@active_required +@login_required +@permission_required('gestion.add_category') +def addCategory(request): + """ + Displays a :class:`gestion.forms.CategoryForm` to add a category. + """ + form = CategoryForm(request.POST or None) + if(form.is_valid()): + category = form.save() + messages.success(request, "La catégorie a bien été ajoutée") + return redirect(reverse('gestion:categoryProfile', kwargs={'pk':category.pk})) + return render(request, "form.html", {"form": form, "form_title": "Ajout d'une catégorie", "form_button": "Ajouter", "form_button_icon": "plus-square"}) + +@active_required +@login_required +@permission_required('gestion.change_category') +def editCategory(request, pk): + """ + Displays a :class:`gestion.forms.CategoryForm` to edit a category. + + pk + The primary key of the the :class:`gestion.models.Category` to edit. + """ + category = get_object_or_404(Category, pk=pk) + form = CategoryForm(request.POST or None, instance=category) + if(form.is_valid()): + form.save() + messages.success(request, "La catégorie a bien été modifiée") + return redirect(reverse('gestion:categoryProfile', kwargs={'pk': category.pk})) + return render(request, "form.html", {"form": form, "form_title": "Modification d'une catégorie", "form_button": "Modifier", "form_button_icon": "pencil-alt"}) + +@active_required +@login_required +@permission_required('gestion.view_category') +def categoriesList(request): + """ + Display the list of :class:`categories `. + """ + categories = Category.objects.all().order_by('order') + return render(request, "gestion/categories_list.html", {"categories": categories}) + +@active_required +@login_required +@permission_required('gestion.view_category') +def searchCategory(request): + """ + Displays a :class:`gestion.forms.SearchCategory` to search a :class:`gestion.models.Category`. + """ + form = SearchCategoryForm(request.POST or None) + if(form.is_valid()): + return redirect(reverse('gestion:categoryProfile', kwargs={'pk': form.cleaned_data['category'].pk })) + return render(request, "form.html", {"form": form, "form_title":"Rechercher une catégorie", "form_button": "Rechercher", "form_button_icon": "search"}) + +@active_required +@login_required +@permission_required('gestion.view_category') +def categoryProfile(request, pk): + """ + Displays the profile of a :class:`gestion.models.Category`. + + pk + The primary key of the :class:`gestion.models.Category` to display profile. + """ + category = get_object_or_404(Category, pk=pk) + return render(request, "gestion/category_profile.html", {"category": category}) + +class CategoriesAutocomplete(autocomplete.Select2QuerySetView): + """ + Autocomplete view for active :class:`categories `. + """ + def get_queryset(self): + qs = Category.objects.all() + if self.q: + qs = qs.filter(name__icontains=self.q) + return qs \ No newline at end of file