Initial commit: Django inventory system
This commit is contained in:
4
myproject/products/utils/__init__.py
Normal file
4
myproject/products/utils/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Utility package for the products app.
|
||||
Contains various helper functions and utilities.
|
||||
"""
|
||||
212
myproject/products/utils/sku_generator.py
Normal file
212
myproject/products/utils/sku_generator.py
Normal 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
|
||||
83
myproject/products/utils/stock_manager.py
Normal file
83
myproject/products/utils/stock_manager.py
Normal 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
|
||||
Reference in New Issue
Block a user