- Реализован импорт Product из CSV/XLSX через Celery с прогресс-баром - Параллельная загрузка фото товаров с внешних URL (масштабируемость до 500+ товаров) - Добавлена модель ProductImportJob для отслеживания статуса импорта - Создан таск download_product_photo_async для загрузки фото в фоне - Интеграция с существующим ImageProcessor (синхронная обработка через use_async=False) - Добавлены view и template для импорта с real-time обновлением через AJAX FIX: Исправлен баг со счётчиком SKU - инкремент только после успешного сохранения - Добавлен SKUCounter.peek_next_value() - возвращает следующий номер БЕЗ инкремента - Добавлен SKUCounter.increment_counter() - инкрементирует счётчик - generate_product_sku() использует peek_next_value() вместо get_next_value() - Добавлен post_save сигнал increment_sku_counter_after_save() для инкремента после создания - Предотвращает пропуски номеров при ошибках валидации (например cost_price NULL) FIX: Исправлена ошибка с is_main в ProductPhoto - ProductPhoto не имеет поля is_main, используется только order - Первое фото (order=0) автоматически считается главным - Удалён параметр is_main из download_product_photo_async и _collect_photo_tasks Изменены файлы: - products/models/base.py - методы для управления счётчиком SKU - products/models/import_job.py - модель для отслеживания импорта - products/services/import_export.py - сервис импорта с поддержкой Celery - products/tasks.py - таски для асинхронного импорта и загрузки фото - products/signals.py - сигнал для инкремента счётчика после сохранения - products/utils/sku_generator.py - использование peek_next_value() - products/views/product_import_views.py - view для импорта - products/templates/products/product_import*.html - UI для импорта - docker/entrypoint.sh - настройка Celery worker (concurrency=4) - requirements.txt - добавлен requests для загрузки фото
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
"""
|
||
Signals для приложения products.
|
||
|
||
Логирует изменения себестоимости товара через CostPriceHistory.
|
||
FIX: SKU counter bug - инкрементирует счётчик после успешного создания Product.
|
||
"""
|
||
|
||
from django.db.models.signals import post_save
|
||
from django.dispatch import receiver
|
||
from django.db import models
|
||
import re
|
||
|
||
from .models import Product, CostPriceHistory, SKUCounter
|
||
|
||
|
||
@receiver(post_save, sender=Product)
|
||
def log_cost_price_changes(sender, instance, created, **kwargs):
|
||
"""
|
||
Логирует изменения себестоимости товара.
|
||
|
||
Срабатывает при создании или обновлении товара.
|
||
Создает запись в CostPriceHistory если себестоимость изменилась.
|
||
"""
|
||
if created:
|
||
# При создании товара себестоимость обычно 0, логируем только если не 0
|
||
if instance.cost_price != 0:
|
||
CostPriceHistory.objects.create(
|
||
product=instance,
|
||
old_cost_price=0,
|
||
new_cost_price=instance.cost_price,
|
||
reason='system',
|
||
notes='Начальная себестоимость при создании товара'
|
||
)
|
||
return
|
||
|
||
# Получаем предыдущее значение себестоимости
|
||
try:
|
||
previous = Product.objects.get(pk=instance.pk)
|
||
old_cost_price = previous.cost_price
|
||
except Product.DoesNotExist:
|
||
# Товар был удален, не логируем
|
||
return
|
||
|
||
# Если себестоимость изменилась, логируем
|
||
if old_cost_price != instance.cost_price:
|
||
CostPriceHistory.objects.create(
|
||
product=instance,
|
||
old_cost_price=old_cost_price,
|
||
new_cost_price=instance.cost_price,
|
||
reason='recalculation',
|
||
notes='Себестоимость пересчитана на основе партий товара'
|
||
)
|
||
|
||
|
||
@receiver(post_save, sender=Product)
|
||
def increment_sku_counter_after_save(sender, instance, created, **kwargs):
|
||
"""
|
||
FIX: SKU counter bug - increment only after successful save
|
||
|
||
Инкрементирует счётчик SKU ПОСЛЕ успешного создания товара с автогенерированным артикулом.
|
||
|
||
Это предотвращает пропуски номеров при ошибках сохранения (например, валидация cost_price).
|
||
Счётчик инкрементируется только если:
|
||
- Товар только что создан (created=True)
|
||
- SKU соответствует автогенерированному формату (PROD-XXXXXX)
|
||
"""
|
||
if not created:
|
||
return
|
||
|
||
# Проверяем что SKU был автогенерирован (формат PROD-XXXXXX)
|
||
if instance.sku and re.match(r'^PROD-\d{6}$', instance.sku):
|
||
# Инкрементируем счётчик product
|
||
SKUCounter.increment_counter('product')
|