diff --git a/myproject/products/models/base.py b/myproject/products/models/base.py index 8901cfb..314fee4 100644 --- a/myproject/products/models/base.py +++ b/myproject/products/models/base.py @@ -193,6 +193,11 @@ class BaseProductEntity(models.Model): """Физическое удаление из БД (необратимо! только для старых товаров)""" super().delete() + @property + def is_active(self): + """Возвращает True если товар активен""" + return self.status == 'active' + def save(self, *args, **kwargs): """ Автогенерация slug из name если не задан. diff --git a/myproject/products/models/photos.py b/myproject/products/models/photos.py index d8c0b25..15aed86 100644 --- a/myproject/products/models/photos.py +++ b/myproject/products/models/photos.py @@ -124,10 +124,14 @@ class BasePhoto(models.Model): Используется только если Celery недоступен. """ from ..utils.image_processor import ImageProcessor + from django.core.files.storage import default_storage entity = self.get_entity() entity_type = self.get_entity_type() + # Сохраняем путь к временному файлу до перезаписи поля image + temp_path = getattr(temp_image, 'name', None) + processed_paths = ImageProcessor.process_image( temp_image, entity_type, @@ -141,10 +145,18 @@ class BasePhoto(models.Model): super().save(update_fields=['image', 'quality_level', 'quality_warning']) + # Удаляем временный файл из temp после успешной обработки + try: + if temp_path and default_storage.exists(temp_path): + default_storage.delete(temp_path) + except Exception: + pass + def delete(self, *args, **kwargs): """Удаляет все версии изображения при удалении фото""" import logging from ..utils.image_processor import ImageProcessor + from django.core.files.storage import default_storage logger = logging.getLogger(__name__) @@ -159,6 +171,15 @@ class BasePhoto(models.Model): entity_id=entity.id, photo_id=self.id ) + + # Если фото так и осталось во временном пути (обработка не завершилась) — удаляем temp файл + try: + if '/temp/' in self.image.name and default_storage.exists(self.image.name): + default_storage.delete(self.image.name) + logger.info(f"[{self.__class__.__name__}.delete] Deleted temp file: {self.image.name}") + except Exception as del_exc: + logger.warning(f"[{self.__class__.__name__}.delete] Could not delete temp file {self.image.name}: {del_exc}") + logger.info(f"[{self.__class__.__name__}.delete] ✓ Все версии изображения удалены") except Exception as e: logger.error( diff --git a/myproject/products/tasks.py b/myproject/products/tasks.py index abece49..0ecb113 100644 --- a/myproject/products/tasks.py +++ b/myproject/products/tasks.py @@ -8,6 +8,7 @@ import logging from celery import shared_task from django.db import connection from django.apps import apps +from django.core.files.storage import default_storage logger = logging.getLogger(__name__) @@ -53,6 +54,9 @@ def process_product_photo_async(self, photo_id, photo_model_class, schema_name): logger.warning(f"[Celery] Photo {photo_id} has no image file") return {'status': 'error', 'reason': 'no_image'} + # Сохраняем путь к временному файлу до перезаписи поля image + temp_path = photo_obj.image.name + # Получаем entity type для правильного пути сохранения entity_type = photo_obj.get_entity_type() @@ -73,6 +77,14 @@ def process_product_photo_async(self, photo_id, photo_model_class, schema_name): photo_obj.quality_warning = processed_paths.get('quality_warning', False) photo_obj.save(update_fields=['image', 'quality_level', 'quality_warning']) + # Удаляем временный файл из temp после успешной обработки + try: + if temp_path and default_storage.exists(temp_path): + default_storage.delete(temp_path) + logger.info(f"[Celery] Deleted temp file: {temp_path}") + except Exception as del_exc: + logger.warning(f"[Celery] Could not delete temp file {temp_path}: {del_exc}") + logger.info(f"[Celery] ✓ Photo {photo_id} processed successfully " f"(quality: {processed_paths.get('quality_level')})") diff --git a/myproject/products/templates/products/product_list.html b/myproject/products/templates/products/product_list.html index 4ce1af1..597b68f 100644 --- a/myproject/products/templates/products/product_list.html +++ b/myproject/products/templates/products/product_list.html @@ -72,10 +72,14 @@ {% endif %} - {% if product.is_active %} - Активен + {% if product.status == 'active' %} + Активный + {% elif product.status == 'archived' %} + Архивный + {% elif product.status == 'discontinued' %} + Снят {% else %} - Неактивен + {{ product.status }} {% endif %} diff --git a/myproject/products/templates/products/productkit_list.html b/myproject/products/templates/products/productkit_list.html index 2d7ebc2..09a47f2 100644 --- a/myproject/products/templates/products/productkit_list.html +++ b/myproject/products/templates/products/productkit_list.html @@ -69,10 +69,14 @@ {% endif %} - {% if kit.is_active %} - Активен + {% if kit.status == 'active' %} + Активный + {% elif kit.status == 'archived' %} + Архивный + {% elif kit.status == 'discontinued' %} + Снят {% else %} - Неактивен + {{ kit.status }} {% endif %} diff --git a/myproject/products/templates/products/products_unified_list.html b/myproject/products/templates/products/products_unified_list.html new file mode 100644 index 0000000..96918ba --- /dev/null +++ b/myproject/products/templates/products/products_unified_list.html @@ -0,0 +1,189 @@ +{% extends 'base.html' %} +{% load quality_tags %} + +{% block title %}Список товаров и комплектов{% endblock %} + +{% block content %} +
+

Товары и Комплекты

+ + +
+
+
+
+ Фильтры +
+ +
+ +
+ +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+
+ + Сброс +
+
+
+
+
+ + {% if items %} +
+ + + + + + + + + + + + + + + + + {% for item in items %} + + + + + + + + + + + + + {% endfor %} + +
ФотоНазваниеАртикулТипКатегорияЦенаВ наличииКомпонентыСтатусДействия
+ {% with photo=item.photos.first %} +
+ {{ item.name }} + {% if item.item_type == 'product' and photo %} + {{ photo|quality_icon_only }} + {% endif %} +
+ {% endwith %} +
+ {{ item.name }} + {{ item.sku }} + {% if item.item_type == 'product' %} + Товар + {% else %} + Комплект + {% endif %} + + {% for category in item.categories.all %} + {{ category.name }} + {% empty %} + - + {% endfor %} + + {% if item.sale_price %} + {{ item.price|floatformat:2 }} руб.
+ {{ item.sale_price|floatformat:2 }} руб. + {% else %} + {{ item.price|floatformat:2 }} руб. + {% endif %} +
+ {% if item.item_type == 'product' %} + {% if item.in_stock %} + Да + {% else %} + Нет + {% endif %} + {% else %} + - + {% endif %} + + {% if item.item_type == 'kit' %} + {{ item.get_total_components_count }} шт + {% else %} + - + {% endif %} + + {{ item.get_status_display }} + +
+ + {% if item.item_type == 'product' and perms.products.change_product %} + + {% elif item.item_type == 'kit' and perms.products.change_productkit %} + + {% endif %} + {% if item.item_type == 'product' and perms.products.delete_product %} + + {% elif item.item_type == 'kit' and perms.products.delete_productkit %} + + {% endif %} +
+
+
+ + {% include 'components/pagination.html' %} + + {% else %} +
+

Товары или комплекты не найдены.

+
+ {% endif %} +
+{% endblock %} diff --git a/myproject/products/views/product_views.py b/myproject/products/views/product_views.py index ac51109..017743e 100644 --- a/myproject/products/views/product_views.py +++ b/myproject/products/views/product_views.py @@ -43,10 +43,17 @@ class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): if category_id: queryset = queryset.filter(categories__id=category_id) - # Фильтр по статусу + # Фильтр по статусу (новая система) status_filter = self.request.GET.get('status') if status_filter: queryset = queryset.filter(status=status_filter) + else: + # Фильтр по is_active для обратной совместимости (старая система) + is_active_filter = self.request.GET.get('is_active') + if is_active_filter == '1': + queryset = queryset.filter(status='active') + elif is_active_filter == '0': + queryset = queryset.filter(status__in=['archived', 'discontinued']) # Фильтр по тегам tags = self.request.GET.getlist('tags') @@ -250,6 +257,7 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV search_query = self.request.GET.get('search') category_id = self.request.GET.get('category') status_filter = self.request.GET.get('status') + is_active_filter = self.request.GET.get('is_active') # Фильтрация по поиску if search_query: @@ -273,10 +281,18 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV products = products.filter(categories__id=category_id) kits = kits.filter(categories__id=category_id) - # Фильтрация по статусу + # Фильтрация по статусу (новая система) if status_filter: products = products.filter(status=status_filter) kits = kits.filter(status=status_filter) + else: + # Фильтрация по is_active для обратной совместимости (старая система) + if is_active_filter == '1': + products = products.filter(status='active') + kits = kits.filter(status='active') + elif is_active_filter == '0': + products = products.filter(status__in=['archived', 'discontinued']) + kits = kits.filter(status__in=['archived', 'discontinued']) # Добавляем type для различения в шаблоне products_list = list(products.order_by('-created_at')) diff --git a/myproject/products/views/productkit_views.py b/myproject/products/views/productkit_views.py index 388e8cf..d735cb4 100644 --- a/myproject/products/views/productkit_views.py +++ b/myproject/products/views/productkit_views.py @@ -37,12 +37,17 @@ class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): if category_id: queryset = queryset.filter(categories__id=category_id) - # Фильтр по статусу - is_active = self.request.GET.get('is_active') - if is_active == '1': - queryset = queryset.filter(is_active=True) - elif is_active == '0': - queryset = queryset.filter(is_active=False) + # Фильтр по статусу (новая система) + status_filter = self.request.GET.get('status') + if status_filter: + queryset = queryset.filter(status=status_filter) + else: + # Фильтр по is_active для обратной совместимости (старая система) + is_active = self.request.GET.get('is_active') + if is_active == '1': + queryset = queryset.filter(status='active') + elif is_active == '0': + queryset = queryset.filter(status__in=['archived', 'discontinued']) return queryset.order_by('-created_at') diff --git a/myproject/templates/components/filter_panel.html b/myproject/templates/components/filter_panel.html index a84da4b..cbf4968 100644 --- a/myproject/templates/components/filter_panel.html +++ b/myproject/templates/components/filter_panel.html @@ -88,13 +88,14 @@ {% if show_status|default:True %}
-
{% endif %}