Initial commit: Django inventory system

This commit is contained in:
2025-10-22 01:11:06 +03:00
commit d78c43d9a9
93 changed files with 9204 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
"""
Utility package for the products app.
Contains various helper functions and utilities.
"""

View File

@@ -0,0 +1,212 @@
"""
Utility functions for generating SKUs for products, kits, and categories.
New SKU format:
- Products: PROD-XXXXXX or PROD-XXXXXX-VARIANT
- Kits: KIT-XXXXXX
- Categories: CAT-XXXX
Examples:
- PROD-000001
- PROD-000002-50
- KIT-000001
- CAT-0001
"""
import re
from string import ascii_uppercase
def parse_variant_suffix(name):
"""
Извлекает суффикс варианта из названия товара.
Поддерживаемые форматы:
- "Роза Freedom 50см" -> "50"
- "Роза Freedom 60 см" -> "60"
- "Лента 2.5м" -> "25" (метры в дециметры)
- "Коробка S" -> "S"
- "Коробка размер M" -> "M"
Args:
name (str): Название товара
Returns:
str or None: Извлеченный суффикс или None
"""
if not name:
return None
# Паттерны для извлечения суффикса
patterns = [
# Размеры в см: "50см", "60 см"
(r'(\d+)\s*см', lambda m: m.group(1)),
# Размеры в метрах: "2.5м" -> "25" (конвертируем в дециметры)
(r'(\d+\.?\d*)\s*м(?:\s|$)', lambda m: str(int(float(m.group(1)) * 10))),
# Буквенные размеры в конце: "S", "M", "L", "XL"
(r'\b([XSML]{1,3})\s*$', lambda m: m.group(1)),
# "размер S", "размер M"
(r'размер\s+([XSML]{1,3})', lambda m: m.group(1)),
# Просто число в конце: "Товар 50"
(r'\s+(\d+)\s*$', lambda m: m.group(1)),
]
for pattern, extractor in patterns:
match = re.search(pattern, name, re.IGNORECASE)
if match:
return extractor(match)
return None
def ensure_sku_unique(base_sku, exclude_id=None, model_type=None):
"""
Проверяет уникальность артикула и добавляет буквенный суффикс при конфликте.
Если артикул уже существует:
PROD-000001 -> PROD-000001A -> PROD-000001B -> ... -> PROD-000001Z
Args:
base_sku (str): Базовый артикул для проверки
exclude_id (int): ID товара/комплекта/категории, который нужно исключить из проверки
model_type (str): Тип модели ('product', 'kit', 'category') для исключения из проверки
Returns:
str: Уникальный артикул
"""
from products.models import Product, ProductKit, ProductCategory
# Проверяем, существует ли базовый артикул
sku = base_sku
# Проверка во всех моделях с артикулами
def sku_exists(sku_to_check):
product_exists = Product.objects.filter(sku=sku_to_check)
if model_type == 'product' and exclude_id:
product_exists = product_exists.exclude(id=exclude_id)
product_exists = product_exists.exists()
kit_exists = ProductKit.objects.filter(sku=sku_to_check)
if model_type == 'kit' and exclude_id:
kit_exists = kit_exists.exclude(id=exclude_id)
kit_exists = kit_exists.exists()
category_exists = ProductCategory.objects.filter(sku=sku_to_check)
if model_type == 'category' and exclude_id:
category_exists = category_exists.exclude(id=exclude_id)
category_exists = category_exists.exists()
return product_exists or kit_exists or category_exists
# Если базовый артикул свободен, возвращаем его
if not sku_exists(sku):
return sku
# Иначе добавляем буквы A-Z
for letter in ascii_uppercase:
sku_with_letter = f"{base_sku}{letter}"
if not sku_exists(sku_with_letter):
return sku_with_letter
# Если все буквы заняты (маловероятно), добавляем AA, AB, и т.д.
for first_letter in ascii_uppercase:
for second_letter in ascii_uppercase:
sku_with_letters = f"{base_sku}{first_letter}{second_letter}"
if not sku_exists(sku_with_letters):
return sku_with_letters
# В крайнем случае возвращаем базовый + timestamp
from django.utils import timezone
return f"{base_sku}-{timezone.now().strftime('%Y%m%d%H%M%S')}"
def generate_product_sku(product):
"""
Генерирует уникальный артикул для товара.
Формат: PROD-XXXXXX или PROD-XXXXXX-VARIANT
Args:
product: Экземпляр модели Product
Returns:
str: Сгенерированный артикул
"""
from products.models import SKUCounter
# Получаем следующий номер из глобального счетчика
next_number = SKUCounter.get_next_value('product')
# Форматируем номер с ведущими нулями (6 цифр)
base_sku = f"PROD-{next_number:06d}"
# Определяем суффикс варианта
variant_suffix = None
# 1. Если суффикс задан вручную в поле variant_suffix
if product.variant_suffix:
variant_suffix = product.variant_suffix.strip()
# 2. Если суффикс не задан, пытаемся извлечь из названия
# (это работает и для товаров в группах вариантов, и без них)
else:
parsed_suffix = parse_variant_suffix(product.name)
if parsed_suffix:
variant_suffix = parsed_suffix
# Добавляем суффикс, если он есть
if variant_suffix:
base_sku = f"{base_sku}-{variant_suffix}"
# Обеспечиваем уникальность
unique_sku = ensure_sku_unique(base_sku, exclude_id=product.id if product.id else None, model_type='product')
return unique_sku
def generate_kit_sku():
"""
Генерирует уникальный артикул для комплекта.
Формат: KIT-XXXXXX
Returns:
str: Сгенерированный артикул
"""
from products.models import SKUCounter
# Получаем следующий номер из глобального счетчика
next_number = SKUCounter.get_next_value('kit')
# Форматируем номер с ведущими нулями (6 цифр)
base_sku = f"KIT-{next_number:06d}"
# Обеспечиваем уникальность
unique_sku = ensure_sku_unique(base_sku, model_type='kit')
return unique_sku
def generate_category_sku():
"""
Генерирует уникальный артикул для категории.
Формат: CAT-XXXX (4 цифры)
Returns:
str: Сгенерированный артикул
"""
from products.models import SKUCounter
# Получаем следующий номер из глобального счетчика
next_number = SKUCounter.get_next_value('category')
# Форматируем номер с ведущими нулями (4 цифры)
base_sku = f"CAT-{next_number:04d}"
# Обеспечиваем уникальность
unique_sku = ensure_sku_unique(base_sku, model_type='category')
return unique_sku

View File

@@ -0,0 +1,83 @@
"""
Менеджер для работы с остатками товаров.
Это заглушка для будущей интеграции с системой складского учёта.
В будущем здесь будет реальная логика проверки остатков на складе.
"""
class StockManager:
"""
Менеджер для работы с остатками товаров (заглушка для будущей реализации).
В будущем этот класс будет интегрирован с реальной системой складского учёта,
чтобы проверять фактические остатки товаров на складе.
"""
def check_stock(self, product, quantity):
"""
Проверяет наличие товара в нужном количестве.
Args:
product: Экземпляр модели Product
quantity: Требуемое количество (Decimal)
Returns:
bool: True если товар доступен в нужном количестве, False иначе
TODO: Интегрировать с реальной системой складского учёта
"""
# Пока всегда возвращаем True (заглушка)
# В будущем здесь будет проверка реальных остатков
return True
def get_available_quantity(self, product):
"""
Возвращает доступное количество товара на складе.
Args:
product: Экземпляр модели Product
Returns:
Decimal: Доступное количество товара
TODO: Интегрировать с реальной системой складского учёта
"""
# Заглушка - возвращаем большое число
# В будущем здесь будет запрос к складской системе
from decimal import Decimal
return Decimal('9999')
def reserve_stock(self, product, quantity, order_id=None):
"""
Резервирует товар под заказ.
Args:
product: Экземпляр модели Product
quantity: Количество для резервирования (Decimal)
order_id: ID заказа (опционально)
Returns:
bool: True если резервирование успешно, False иначе
TODO: Интегрировать с реальной системой складского учёта
"""
# Заглушка
return True
def release_stock(self, product, quantity, order_id=None):
"""
Освобождает зарезервированный товар.
Args:
product: Экземпляр модели Product
quantity: Количество для освобождения (Decimal)
order_id: ID заказа (опционально)
Returns:
bool: True если освобождение успешно, False иначе
TODO: Интегрировать с реальной системой складского учёта
"""
# Заглушка
return True