From c0fe3e1db70bdba85cf382624817d8d8ea9d6f85 Mon Sep 17 00:00:00 2001 From: spl3g Date: Mon, 1 Jun 2026 21:23:12 +0300 Subject: add orders list, update, create --- config/urls.py | 8 +++ core/forms.py | 103 +++++++++++++++++++++++++++++++ core/management/commands/import_data.py | 14 ++--- core/models.py | 27 ++++++++- core/templates/core/base.html | 2 +- core/templates/core/order_form.html | 20 ++++++ core/templates/core/order_list.html | 32 ++++++++++ core/templates/core/product_list.html | 5 +- core/views.py | 104 +++++++++++++++++++++++++++++++- pyproject.toml | 1 + static/css/style.css | 81 +++++++++++++++++++++++++ static/images/picture.png | Bin 0 -> 13344 bytes uv.lock | 14 +++++ 13 files changed, 397 insertions(+), 14 deletions(-) create mode 100644 core/forms.py create mode 100644 core/templates/core/order_form.html create mode 100644 core/templates/core/order_list.html create mode 100644 static/css/style.css create mode 100644 static/images/picture.png diff --git a/config/urls.py b/config/urls.py index 1808f0f..858536b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -24,6 +24,10 @@ from core.views import ( UserLoginView, ProductListView, ProductCreateView, + ProductUpdateView, + OrderListView, + OrderCreateView, + OrderUpdateView, ) urlpatterns = [ @@ -32,6 +36,10 @@ urlpatterns = [ path("logout/", LogoutView.as_view(), name="logout"), path("products/", ProductListView.as_view(), name="product_list"), path("products/add/", ProductCreateView.as_view(), name="product_create"), + path("products//edit/", ProductUpdateView.as_view(), name="product_edit"), + path("orders/", OrderListView.as_view(), name="order_list"), + path("orders/add/", OrderCreateView.as_view(), name="order_create"), + path("orders//edit/", OrderUpdateView.as_view(), name="order_update"), ] if settings.DEBUG: diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..1ade0fe --- /dev/null +++ b/core/forms.py @@ -0,0 +1,103 @@ +from django import forms +from django.forms import inlineformset_factory + +from .models import Product, Supplier, Order, OrderItem +from extra_views import InlineFormSetFactory + +class ProductForm(forms.ModelForm): + supplier_name = forms.CharField(label="Поставщик", required=True) + + class Meta: + model = Product + fields = [ + "article", + "name", + "unit", + "price", + "manufacturer", + "category", + "discount", + "quantity", + "description", + "photo", + ] + labels = { + "article": "Артикуль", + "name": "Название", + "unit": "Единица измерения", + "price": "Цена", + "manufacturer": "Производитель", + "category": "Категория", + "discount": "Скидка", + "quantity": "Колличество", + "description": "Описание", + "photo": "Фото", + } + widgets = { + "description": forms.Textarea(attrs={"rows": 3}) + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + if self.instance.supplier: + self.fields["supplier_name"].initial = self.instance.supplier.name + + def save(self, commit=True): + supplier, _ = Supplier.objects.get_or_create( + name=self.cleaned_data["supplier_name"].strip() + ) + + instance = super().save(commit=False) + instance.supplier = supplier + + if commit: + instance.save() + + return instance + + + def clean_price(self): + price = self.cleaned_data.get("price") + if price < 0: + raise forms.ValidationError("Цена не может быть отрицательной") + return price + + def clean_quantity(self): + quantity = self.cleaned_data.get("quantity") + if quantity < 0: + raise forms.ValidationError("Колличество не может быть отрицательной") + return quantity + + +class OrderForm(forms.ModelForm): + class Meta: + model = Order + fields = [ + "delivery_date", + "pickup_point", + "pickup_code", + "status", + ] + + labels = { + "order_date": "Дата заказа", + "delivery_date": "Дата доставки", + "pickup_point": "Пункт выдачи", + "client_name": "Имя клиента", + "pickup_code": "Код выдачи", + "status": "Статус", + "items": "Продукты", + } + widgets = { + "delivery_date": forms.SelectDateWidget + } + +class OrderItemFormSet(InlineFormSetFactory): + model = OrderItem + fields = ["product", "count"] + factory_kwargs = { + "extra": 1, + "can_delete": True, + "labels": {"product": "Продукт", "count": "Количество"} + } diff --git a/core/management/commands/import_data.py b/core/management/commands/import_data.py index f78f27e..cf1d4b6 100644 --- a/core/management/commands/import_data.py +++ b/core/management/commands/import_data.py @@ -79,6 +79,8 @@ class Command(BaseCommand): else PickupPoint.objects.first() ) + status = "completed" if row.pop("status", None) == "Завершен" else "new" + user_obj = User.objects.get(full_name=row["client_name"]) order, created = Order.objects.get_or_create( id=row["id"], @@ -86,16 +88,14 @@ class Command(BaseCommand): user=user_obj, delivery_date=delivery_date, order_date=order_date, + status=status, defaults=row, ) if created and items_string: items = items_string.split(", ") for i in range(0, len(items), 2): art = items[i].strip() - try: - prod = Products.get(article=art) - OrderItem.objects.create( - order=order, product=prod, count=int(items[i+1]) - ) - except Exception: - pass + prod = Product.objects.get(article=art) + OrderItem.objects.create( + order=order, product=prod, count=int(items[i+1]) + ) diff --git a/core/models.py b/core/models.py index db8b4c1..ee673ff 100644 --- a/core/models.py +++ b/core/models.py @@ -1,5 +1,10 @@ from django.contrib.auth.models import AbstractUser from django.db import models +from PIL import Image +from io import BytesIO +from django.core.files.base import ContentFile +import os + class Role(models.Model): name = models.CharField(max_length=50, unique=True, verbose_name="Name of the role") @@ -43,8 +48,23 @@ class Product(models.Model): this = Product.objects.get(id=self.id) if this.photo and self.photo and this.photo != self.photo: this.photo.delete(save=False) + except Exception: pass + + if self.photo: + img = Image.open(self.photo) + img.thumbnail((300, 200)) + + output = BytesIO() + img.save(output, img.format) + output.seek(0) + self.photo.save( + os.path.basename(self.photo.name), + output, + save=False + ) + super().save(*args, **kwargs) @property @@ -56,12 +76,17 @@ class Product(models.Model): return self.name class Order(models.Model): + STATUS_CHOICES = ( + ("new", "Новый"), + ("completed", "Завершен"), + ) + order_date = models.DateTimeField(auto_now_add=True) delivery_date = models.DateTimeField() pickup_point = models.ForeignKey(PickupPoint, on_delete=models.CASCADE) client_name = models.CharField(max_length=255, null=True, blank=True) pickup_code = models.IntegerField() - status = models.CharField(max_length=50, default="Новый") + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default="Новый") user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) class OrderItem(models.Model): diff --git a/core/templates/core/base.html b/core/templates/core/base.html index 56d9e17..74aba44 100644 --- a/core/templates/core/base.html +++ b/core/templates/core/base.html @@ -14,7 +14,7 @@
{% if user.is_authenticated %} - {{ user.role.name }} + {{ user.role.name }} | {{ user.full_name }}
{% csrf_token %} diff --git a/core/templates/core/order_form.html b/core/templates/core/order_form.html new file mode 100644 index 0000000..b46b593 --- /dev/null +++ b/core/templates/core/order_form.html @@ -0,0 +1,20 @@ +{% extends 'core/base.html' %} + +{% block title %}{% if is_edit %}Редактирование{% else %}Добавление{% endif %} заказа - ОООБувь{% endblock %} + +{% block h1 %}{% if is_edit %}Редактирование{% else %}Добавление{% endif %} заказа{% endblock %} + +{% block content %} +
+ + {% csrf_token %} + {{ form }} + +

Товары в заказе

+ {% for formset in inlines %} + {{ formset }} + {% endfor %} + + +
+{% endblock %} diff --git a/core/templates/core/order_list.html b/core/templates/core/order_list.html new file mode 100644 index 0000000..8fe80bd --- /dev/null +++ b/core/templates/core/order_list.html @@ -0,0 +1,32 @@ +{% extends 'core/base.html' %} + +{% block title %}Заказы - ООООБУВЬ {% endblock %} + +{% block h1 %}Заказы{% endblock %} + +{% block content %} +{% if user.role.name == "Администратор" or user.role.name == "Менеджер" %} +
+ Добавить заказ +
+{% endif %} +
+ {% for order in orders %} +
+
+ + {% for orderitem in order.items.all %} + {{orderitem.product.article}} + {% endfor %} +
+ Пункт выдачи: {{ order.pickup_point.address }}
+ Статус: {{ order.status }}
+ {% if user.role.name == "Администратор" or user.role.name == "Менеджер" %} + Редактировать заказ + {% endif %} +
+
{{ order.delivery_date }}
+
+ {% endfor %} +
+{% endblock %} diff --git a/core/templates/core/product_list.html b/core/templates/core/product_list.html index ad7a5bf..b2aa6e0 100644 --- a/core/templates/core/product_list.html +++ b/core/templates/core/product_list.html @@ -33,11 +33,9 @@
- {% comment %}
Заказы
- {% endcomment %} {% if user.role.name == 'Администратор' %}
@@ -75,6 +73,9 @@ Единица измерения: {{ product.unit }}
Колличество на складе: {{ product.discount }} + {% if user.role.name == 'Администратор' %} +
Редактировать товар + {% endif%}
{{ product.discount }}
diff --git a/core/views.py b/core/views.py index f090d81..0e62eaf 100644 --- a/core/views.py +++ b/core/views.py @@ -7,9 +7,12 @@ from django.contrib.auth.views import LoginView from django.contrib import messages from django.db.models import Q from django.contrib.auth.mixins import UserPassesTestMixin +from django.http import HttpResponseRedirect +from extra_views import CreateWithInlinesView, UpdateWithInlinesView +from django import forms -from .models import Product, Supplier -from .forms import ProductForm +from .models import Product, Supplier, Order, OrderItem +from .forms import ProductForm, OrderForm, OrderItemFormSet class UserLoginView(LoginView): template_name = "core/login.html" @@ -54,7 +57,7 @@ class AdminRequiredMixin(UserPassesTestMixin): return ( self.request.user.is_authenticated and self.request.user.role - and self.request.user.role == "Администратор" + and self.request.user.role.name == "Администратор" ) class ProductCreateUpdateMixin: @@ -72,3 +75,98 @@ class ProductCreateView(AdminRequiredMixin, ProductCreateUpdateMixin, CreateView def form_valid(self, form): messages.success(self.request, "Товар успешно добавлен") return super().form_valid(form) + +class ProductUpdateView(AdminRequiredMixin, ProductCreateUpdateMixin, UpdateView): + model = Product + form_class = ProductForm + template_name = "core/product_form.html" + success_url = reverse_lazy("product_list") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["is_edit"] = True + return context + + +class AdminManagerRequiredMixin(UserPassesTestMixin): + def test_func(self): + return ( + self.request.user.is_authenticated + and self.request.user.role + and (self.request.user.role.name == "Администратор" or self.request.user.role.name == "Менеджер") + ) + +class OrderListView(AdminManagerRequiredMixin, ListView): + model = Order + template_name = "core/order_list.html" + context_object_name = "orders" + + +class OrderCreateView(CreateWithInlinesView): + model = Order + form_class = OrderForm + inlines = [OrderItemFormSet] + success_url = reverse_lazy("order_list") + +class OrderUpdateView(UpdateWithInlinesView): + model = Order + form_class = OrderForm + inlines = [OrderItemFormSet] + success_url = reverse_lazy("order_list") + +# formset variant +# class OrderCreateView(CreateView): +# model = Order +# form_class = OrderForm +# template_name = "core/order_form.html" +# success_url = reverse_lazy("order_list") + +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# if self.request.POST: +# # re-use the bound formset if post() already stashed an invalid one +# context["formset"] = getattr(self, "formset", OrderItemFormSet(self.request.POST)) +# else: +# context["formset"] = OrderItemFormSet() +# return context + +# def post(self, request, *args, **kwargs): +# self.object = None +# form = self.get_form() +# formset = OrderItemFormSet(request.POST) +# if form.is_valid() and formset.is_valid(): +# self.object = form.save() +# formset.instance = self.object +# formset.save() +# return HttpResponseRedirect(self.get_success_url()) +# # stash the invalid formset so get_context_data renders it with errors +# self.formset = formset +# return self.form_invalid(form) + + +# class OrderUpdateView(UpdateView): +# model = Order +# form_class = OrderForm +# template_name = "core/order_form.html" +# success_url = reverse_lazy("order_list") + +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# if self.request.POST: +# context["formset"] = getattr( +# self, "formset", OrderItemFormSet(self.request.POST, instance=self.object) +# ) +# else: +# context["formset"] = OrderItemFormSet(instance=self.object) +# return context + +# def post(self, request, *args, **kwargs): +# self.object = self.get_object() +# form = self.get_form() +# formset = OrderItemFormSet(request.POST, instance=self.object) +# if form.is_valid() and formset.is_valid(): +# form.save() +# formset.save() +# return HttpResponseRedirect(self.get_success_url()) +# self.formset = formset +# return self.form_invalid(form) diff --git a/pyproject.toml b/pyproject.toml index f33ff74..46bdfb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "django>=6.0.5", + "django-extra-views>=0.16.0", "pillow>=12.2.0", "psycopg2-binary>=2.9.12", ] diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..a1c6783 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,81 @@ +body { + font-family: "Times New Roman", serif; +} + +header { + display: flex; + justify-content: justify-between; + align-items: center; + align-content: center; + background-color: #CACF; + margin: 10px; +} + +.header-left { + font-size: xxx-large; + padding: 10px; +} + +.login-button { + background: none; + border: none; + color: blue; + text-decoration: underline; + cursor: pointer; + padding: 0; + font-family: inherit; + font-size: inherit; +} + +.product-card { + border: solid black 2px; + display: flex; + padding: 10px; +} + +.product-card.sale { + background-color: #2e8b57; +} + +.product-card.out-of-stock { + background-color: aqua; +} + +.product-card .image { + width: 30%; + border: 2px gray solid; +} + +.product-card .details { + flex: 1; + border: solid black 1px; + margin: 0 10px; + padding: 5px; +} + +.product-card .sale { + border: solid black 1px; + + font-weight: bold; + font-size: x-large; + padding: auto; + + display: flex; + align-items: center; + justify-content: center; + width: 15%; +} + +.product-card .delivery { + border: solid black 1px; + padding: auto; + display: flex; + align-items: center; + justify-content: center; + width: 15%; +} + +.old-price { + text-decoration: line-through; + color: red; +} diff --git a/static/images/picture.png b/static/images/picture.png new file mode 100644 index 0000000..1cd0789 Binary files /dev/null and b/static/images/picture.png differ diff --git a/uv.lock b/uv.lock index 0c13c35..2fedd95 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "django" }, + { name = "django-extra-views" }, { name = "pillow" }, { name = "psycopg2-binary" }, ] @@ -24,6 +25,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "django", specifier = ">=6.0.5" }, + { name = "django-extra-views", specifier = ">=0.16.0" }, { name = "pillow", specifier = ">=12.2.0" }, { name = "psycopg2-binary", specifier = ">=2.9.12" }, ] @@ -42,6 +44,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/5b/1328f8b84fce040c404f76822bf8c57d254e368e8cbd8bd67ec2b26d75f5/django-6.0.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0", size = 8368680, upload-time = "2026-05-05T13:54:33.532Z" }, ] +[[package]] +name = "django-extra-views" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/61/c6d2ced11fc4235d2aeb422ad5af3b516effd942e2be5a263a5d93dcb1e2/django_extra_views-0.16.0.tar.gz", hash = "sha256:7f8e07bd6c9388816a7c08d752661172d4078758c2079fc0cadddfcf5cd38ae3", size = 13235, upload-time = "2025-04-22T15:10:58.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/6a/511cf7ad5ac8d0890eb7ec374668759e9fbb263af98abb516fdc14be2633/django_extra_views-0.16.0-py2.py3-none-any.whl", hash = "sha256:af990d1779813b728231e94bcc703614fef60015c4c31d53185c5f4f898a37be", size = 15344, upload-time = "2025-04-22T15:10:56.814Z" }, +] + [[package]] name = "pillow" version = "12.2.0" -- cgit v1.2.3