Очистка management commands: удалены неиспользуемые скрипты cleanup_photo_media, demo_variants, fix_category_slugs. Обновлены команды recalculate_product_costs и update_product_in_stock

This commit is contained in:
2026-01-06 14:11:24 +03:00
parent 5d6b894ca6
commit e5ec82d7d2
5 changed files with 77 additions and 415 deletions

View File

@@ -1,125 +0,0 @@
"""
Management команда для очистки старых файлов фотографий.
Удаляет файлы с суффиксами (например, original_b374WLW.jpg) которые появляются при коллизии имен.
"""
import os
import logging
from django.core.management.base import BaseCommand
from django.core.files.storage import default_storage
from products.models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'Очистить старые файлы фотографий с суффиксами коллизии имен'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Показать что будет удалено без реального удаления',
)
parser.add_argument(
'--fix-db',
action='store_true',
help='Также проверить и исправить пути в БД если нужно',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
fix_db = options['fix_db']
self.stdout.write(self.style.SUCCESS('=== Очистка старых файлов фотографий ===\n'))
if dry_run:
self.stdout.write(self.style.WARNING('⚠️ РЕЖИМ ТЕСТИРОВАНИЯ (--dry-run) - ничего не будет удалено\n'))
# Получаем все фотографии из БД
photo_models = [ProductPhoto, ProductKitPhoto, ProductCategoryPhoto]
total_photos = sum(model.objects.count() for model in photo_models)
self.stdout.write(f'Найдено {total_photos} фотографий в БД\n')
deleted_count = 0
fixed_count = 0
# Проходим по всем фотографиям
for model_class in photo_models:
model_name = model_class.__name__
self.stdout.write(f'\n--- {model_name} ---')
for photo in model_class.objects.all():
if not photo.image or not photo.image.name:
continue
# Получаем основной путь (без расширения)
image_path = photo.image.name
base_parts = image_path.rsplit('.', 1)
base_path = base_parts[0]
# Ищем все файлы с этим base_path
directory = os.path.dirname(image_path)
if not directory:
continue
try:
# Список всех файлов в папке
if hasattr(default_storage, '_storage'):
# Для файловой системы
full_dir = os.path.join(default_storage.location, directory)
if os.path.exists(full_dir):
files = os.listdir(full_dir)
else:
continue
else:
continue
# Анализируем файлы в папке
expected_files = {
'original.jpg', 'original.webp', 'original.png',
'large.webp', 'medium.webp', 'thumb.webp',
}
# Находим лишние файлы (с суффиксами)
for filename in files:
full_path = os.path.join(full_dir, filename)
file_path = os.path.join(directory, filename)
# Проверяем есть ли суффикс коллизии (например, _b374WLW)
if filename not in expected_files:
# Это потенциальный старый файл с суффиксом
self.stdout.write(f' Найден старый файл: {file_path}')
if not dry_run:
try:
if os.path.exists(full_path):
os.remove(full_path)
deleted_count += 1
self.stdout.write(
self.style.WARNING(f' ✓ Удален')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f' ✗ Ошибка при удалении: {str(e)}')
)
else:
deleted_count += 1
self.stdout.write(
self.style.WARNING(f' [dry-run] Будет удален')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f' Ошибка при обработке {image_path}: {str(e)}')
)
# Вывод итогов
self.stdout.write(f'\n\n=== ИТОГИ ===')
self.stdout.write(self.style.SUCCESS(f'✓ Обработано файлов с суффиксами: {deleted_count}'))
if dry_run:
self.stdout.write(self.style.WARNING(
'\n⚠️ Это был тестовый запуск. Запустите без --dry-run чтобы реально удалить файлы.'
))

View File

@@ -1,243 +0,0 @@
"""
Management команда для демонстрации работы системы вариантов товаров.
Использование:
python manage.py demo_variants
"""
from decimal import Decimal
from django.core.management.base import BaseCommand
from products.models import (
Product, ProductKit, KitItem, ProductCategory,
ProductVariantGroup, KitItemPriority
)
from products.utils.stock_manager import StockManager
class Command(BaseCommand):
help = 'Демонстрация работы системы вариантов товаров'
def handle(self, *args, **options):
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.SUCCESS("ДЕМОНСТРАЦИЯ СИСТЕМЫ ВАРИАНТОВ ТОВАРОВ"))
self.stdout.write("="*60 + "\n")
# Создаём демо-данные
group, rose_50, rose_60, rose_70 = self.create_variant_group()
# Создаём букеты
premium_kit = self.create_premium_bouquet(group, rose_50, rose_60, rose_70)
economy_kit = self.create_economy_bouquet(group, rose_50, rose_60, rose_70)
# Проверяем доступность
self.check_availability(premium_kit, economy_kit)
# Получаем лучший товар
self.show_best_product(premium_kit)
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.SUCCESS("ДЕМОНСТРАЦИЯ ЗАВЕРШЕНА"))
self.stdout.write("="*60 + "\n")
def create_variant_group(self):
"""Создание группы вариантов"""
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.HTTP_INFO("ШАГ 1: Создание группы вариантов"))
self.stdout.write("="*60)
# Создаём категорию
category, _ = ProductCategory.objects.get_or_create(
name="Цветы",
defaults={'slug': 'cvety'}
)
# Создаём товары - розы разной длины
rose_50, _ = Product.objects.get_or_create(
name="Роза Freedom 50см красная",
defaults={
'cost_price': Decimal('80.00'),
'sale_price': Decimal('100.00'),
'category': category
}
)
rose_60, _ = Product.objects.get_or_create(
name="Роза Freedom 60см красная",
defaults={
'cost_price': Decimal('120.00'),
'sale_price': Decimal('150.00'),
'category': category
}
)
rose_70, _ = Product.objects.get_or_create(
name="Роза Freedom 70см красная",
defaults={
'cost_price': Decimal('160.00'),
'sale_price': Decimal('200.00'),
'category': category
}
)
# Создаём группу вариантов
group, created = ProductVariantGroup.objects.get_or_create(
name="Роза красная Freedom",
defaults={
'description': 'Красная роза Freedom различной длины (50-70см)'
}
)
# Добавляем товары в группу
rose_50.variant_groups.add(group)
rose_60.variant_groups.add(group)
rose_70.variant_groups.add(group)
self.stdout.write(self.style.SUCCESS(f"[OK] Создана группа: {group.name}"))
self.stdout.write(f" Товаров в группе: {group.get_products_count()}")
self.stdout.write(" Товары:")
for product in group.products.all():
self.stdout.write(f" - {product.name} ({product.sale_price} руб.)")
return group, rose_50, rose_60, rose_70
def create_premium_bouquet(self, group, rose_50, rose_60, rose_70):
"""Создание премиум букета"""
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.HTTP_INFO("ШАГ 2: Создание премиум букета"))
self.stdout.write("="*60)
# Создаём букет
kit, _ = ProductKit.objects.get_or_create(
name="Ранчо Виталия Премиум",
defaults={
'slug': 'rancho-vitaliya-premium',
'pricing_method': 'from_sale_prices'
}
)
# Создаём позицию с группой вариантов
kit_item, _ = KitItem.objects.get_or_create(
kit=kit,
variant_group=group,
defaults={
'quantity': Decimal('15.000')
}
)
# Настраиваем приоритеты (для премиум букета - сначала длинные)
KitItemPriority.objects.get_or_create(
kit_item=kit_item,
product=rose_70,
defaults={'priority': 0}
)
KitItemPriority.objects.get_or_create(
kit_item=kit_item,
product=rose_60,
defaults={'priority': 1}
)
KitItemPriority.objects.get_or_create(
kit_item=kit_item,
product=rose_50,
defaults={'priority': 2}
)
self.stdout.write(self.style.SUCCESS(f"[OK] Создан букет: {kit.name}"))
self.stdout.write(f" Позиций: {kit.get_total_components_count()}")
self.stdout.write(f" С вариантами: {kit.get_components_with_variants_count()}")
self.stdout.write(f"\n Приоритеты для позиции '{kit_item.get_display_name()}':")
for priority in kit_item.priorities.all().order_by('priority'):
self.stdout.write(f" {priority.priority}. {priority.product.name} - {priority.product.sale_price} руб.")
return kit
def create_economy_bouquet(self, group, rose_50, rose_60, rose_70):
"""Создание эконом букета"""
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.HTTP_INFO("ШАГ 3: Создание эконом букета"))
self.stdout.write("="*60)
kit, _ = ProductKit.objects.get_or_create(
name="Ранчо Виталия Эконом",
defaults={
'slug': 'rancho-vitaliya-econom',
'pricing_method': 'from_sale_prices'
}
)
kit_item, _ = KitItem.objects.get_or_create(
kit=kit,
variant_group=group,
defaults={
'quantity': Decimal('15.000')
}
)
# Для эконом букета - сначала короткие (дешевые)
KitItemPriority.objects.get_or_create(
kit_item=kit_item,
product=rose_50,
defaults={'priority': 0}
)
KitItemPriority.objects.get_or_create(
kit_item=kit_item,
product=rose_60,
defaults={'priority': 1}
)
KitItemPriority.objects.get_or_create(
kit_item=kit_item,
product=rose_70,
defaults={'priority': 2}
)
self.stdout.write(self.style.SUCCESS(f"[OK] Создан букет: {kit.name}"))
self.stdout.write(f"\n Приоритеты для позиции '{kit_item.get_display_name()}':")
for priority in kit_item.priorities.all().order_by('priority'):
self.stdout.write(f" {priority.priority}. {priority.product.name} - {priority.product.sale_price} руб.")
return kit
def check_availability(self, premium_kit, economy_kit):
"""Проверка доступности"""
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.HTTP_INFO("ШАГ 4: Проверка доступности букетов"))
self.stdout.write("="*60)
stock_manager = StockManager()
# Проверяем премиум букет
self.stdout.write(f"\nПремиум букет: {premium_kit.name}")
if premium_kit.check_availability(stock_manager):
self.stdout.write(self.style.SUCCESS(" [OK] Доступен для сборки"))
price = premium_kit.calculate_price_with_substitutions(stock_manager)
self.stdout.write(f" Цена: {price} руб.")
else:
self.stdout.write(self.style.ERROR(" [ERROR] Недоступен"))
# Проверяем эконом букет
self.stdout.write(f"\nЭконом букет: {economy_kit.name}")
if economy_kit.check_availability(stock_manager):
self.stdout.write(self.style.SUCCESS(" [OK] Доступен для сборки"))
price = economy_kit.calculate_price_with_substitutions(stock_manager)
self.stdout.write(f" Цена: {price} руб.")
else:
self.stdout.write(self.style.ERROR(" [ERROR] Недоступен"))
def show_best_product(self, kit):
"""Показать лучший доступный товар"""
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.HTTP_INFO("ШАГ 5: Выбор лучшего доступного товара"))
self.stdout.write("="*60)
stock_manager = StockManager()
for kit_item in kit.kit_items.all():
self.stdout.write(f"\nПозиция: {kit_item.get_display_name()}")
self.stdout.write(f"Количество: {kit_item.quantity}")
best_product = kit_item.get_best_available_product(stock_manager)
if best_product:
self.stdout.write(self.style.SUCCESS(f"[OK] Лучший доступный товар: {best_product.name}"))
self.stdout.write(f" Цена: {best_product.sale_price} руб.")
self.stdout.write(f" Стоимость позиции: {best_product.sale_price * kit_item.quantity} руб.")
else:
self.stdout.write(self.style.ERROR("[ERROR] Нет доступных товаров"))

View File

@@ -1,34 +0,0 @@
from django.core.management.base import BaseCommand
from django.utils.text import slugify
from unidecode import unidecode
from products.models import ProductCategory
class Command(BaseCommand):
help = 'Fixes category slugs by converting Cyrillic to Latin transliteration'
def handle(self, *args, **options):
categories = ProductCategory.objects.all()
fixed_count = 0
for category in categories:
old_slug = category.slug
# Generate new slug with Latin transliteration
transliterated_name = unidecode(category.name)
new_slug = slugify(transliterated_name)
if old_slug != new_slug:
category.slug = new_slug
category.save()
self.stdout.write(
self.style.SUCCESS(
f'Fixed: "{category.name}" | {old_slug} -> {new_slug}'
)
)
fixed_count += 1
else:
self.stdout.write(f'OK: "{category.name}" | {old_slug}')
self.stdout.write(
self.style.SUCCESS(f'\nTotal fixed: {fixed_count} categories')
)

View File

@@ -1,15 +1,51 @@
"""
Команда управления для пересчёта себестоимости (cost_price) всех товаров.
Использование (для multi-tenant проекта):
python manage.py recalculate_product_costs --schema=grach
python manage.py recalculate_product_costs --schema=grach --verbose
python manage.py recalculate_product_costs --schema=grach --dry-run
НАЗНАЧЕНИЕ:
Команда для разового массового пересчёта себестоимости товаров.
В обычных условиях себестоимость обновляется автоматически через сигналы
(inventory/signals.py: update_product_cost_on_batch_change).
Описание:
Пересчитывает Product.cost_price на основе активных партий (StockBatch).
Использует средневзвешенную стоимость по FIFO принципу.
Товар без партий получает cost_price = 0.00.
КОГДА ИСПОЛЬЗОВАТЬ:
- После миграции данных или массового импорта товаров
- После изменения партий (StockBatch) напрямую в БД (минуя ORM)
- При обнаружении расхождений между кешированной и реальной стоимостью
- После восстановления из резервной копии или исправления данных
- Для проверки корректности расчётов (режим --dry-run)
АВТОМАТИЗАЦИЯ:
В продакшене работают сигналы - команда НЕ нужна регулярно
✓ Сигналы обновляют cost_price при каждом изменении партий
✓ См. inventory/signals.py (строки 1752-1812):
- update_product_cost_on_batch_change (создание/изменение партии)
- update_product_cost_on_batch_delete (удаление партии)
✓ Калькулятор: products/services/cost_calculator.py
ИСПОЛЬЗОВАНИЕ (multi-tenant проект):
# Обычный запуск (сохраняет изменения в БД)
python switch_to_tenant.py anatol recalculate_product_costs
python switch_to_tenant.py grach recalculate_product_costs
# Проверка без сохранения (безопасно - рекомендуется сначала)
python switch_to_tenant.py anatol recalculate_product_costs --dry-run
# С подробным выводом всех товаров
python switch_to_tenant.py anatol recalculate_product_costs --verbose
# Показать только изменённые товары
python switch_to_tenant.py anatol recalculate_product_costs --only-changed
# Комбинация флагов
python switch_to_tenant.py anatol recalculate_product_costs --dry-run --verbose --only-changed
# ❌ НЕПРАВИЛЬНО: python manage.py recalculate_product_costs --schema=anatol
# ✓ ПРАВИЛЬНО: python switch_to_tenant.py anatol recalculate_product_costs
ЛОГИКА:
Рассчитывает средневзвешенную себестоимость на основе активных партий:
cost_price = SUM(quantity * cost_price) / SUM(quantity)
Если у товара нет активных партий с остатками - cost_price = 0.00
"""
from django.core.management.base import BaseCommand

View File

@@ -1,12 +1,40 @@
"""
Команда управления для пересчёта статуса in_stock всех товаров.
Использование:
python manage.py update_product_in_stock
НАЗНАЧЕНИЕ:
Команда для разового массового исправления флага наличия товаров.
В обычных условиях статус обновляется автоматически через сигналы
(inventory/signals.py: update_product_in_stock_on_stock_change).
Описание:
Пересчитывает Product.in_stock на основе текущих остатков в Stock.
Товар считается в наличии если есть хотя бы один Stock с quantity_available > 0.
КОГДА ИСПОЛЬЗОВАТЬ:
- После миграции данных или импорта товаров
- После ручного изменения Stock в БД (минуя ORM)
- При обнаружении рассинхронизации данных
- После восстановления из резервной копии
АВТОМАТИЗАЦИЯ:
В продакшене работают сигналы - команда НЕ нужна регулярно
✓ Сигналы обновляют in_stock при каждом изменении Stock
✓ См. inventory/signals.py (строки 1696-1744):
- update_product_in_stock_on_stock_change
- update_product_in_stock_on_stock_delete
ИСПОЛЬЗОВАНИЕ:
# ДЛЯ SINGLE-TENANT (без мультитенантности):
python manage.py update_product_in_stock
python manage.py update_product_in_stock --verbose
# ДЛЯ MULTI-TENANT (обязательно через switch_to_tenant.py):
python switch_to_tenant.py anatol update_product_in_stock
python switch_to_tenant.py anatol update_product_in_stock --verbose
python switch_to_tenant.py grach update_product_in_stock
# ❌ НЕПРАВИЛЬНО: python manage.py update_product_in_stock --schema=anatol
# ✓ ПРАВИЛЬНО: python switch_to_tenant.py anatol update_product_in_stock
ЛОГИКА:
Товар считается в наличии (in_stock=True), если существует хотя бы
один Stock с quantity_available > 0 на любом складе.
"""
from django.core.management.base import BaseCommand