Files
octopus/myproject/orders/models.py
Andrey Smakotin 097d4ea304 feat: Добавить систему мультитенантности с регистрацией магазинов
Реализована полноценная система мультитенантности на базе django-tenants.
Каждый магазин получает изолированную схему БД и поддомен.

Основные компоненты:

Django-tenants интеграция:
- Модели Client (тенант) и Domain в приложении tenants/
- Разделение на SHARED_APPS и TENANT_APPS
- Public schema для общей админки
- Tenant schemas для изолированных данных магазинов

Система регистрации магазинов:
- Публичная форма регистрации на /register/
- Модель TenantRegistration для заявок со статусами (pending/approved/rejected)
- Валидация schema_name (латиница, 3-63 символа, уникальность)
- Проверка на зарезервированные имена (admin, api, www и т.д.)
- Админ-панель для модерации заявок с кнопками активации/отклонения

Система подписок:
- Модель Subscription с планами (триал 90 дней, месяц, квартал, год)
- Автоматическое создание триальной подписки при активации
- Методы is_expired() и days_left() для проверки статуса
- Цветовая индикация в админке (зеленый/оранжевый/красный)

Приложения:
- tenants/ - управление тенантами, регистрация, подписки
- shops/ - точки магазинов/самовывоза (tenant app)
- Обновлены миграции для всех приложений

Утилиты:
- switch_to_tenant.py - переключение между схемами тенантов
- Обновлены image_processor и image_service

Конфигурация:
- urls_public.py - роуты для public schema (админка + регистрация)
- urls.py - роуты для tenant schemas (магазины)
- requirements.txt - добавлены django-tenants, django-environ, phonenumber-field

Документация:
- DJANGO_TENANTS_SETUP.md - настройка мультитенантности
- TENANT_REGISTRATION_GUIDE.md - руководство по регистрации
- QUICK_START.md - быстрый старт
- START_HERE.md - общая документация

Использование:
1. Пользователь: http://localhost:8000/register/ → заполняет форму
2. Админ: http://localhost:8000/admin/ → активирует заявку
3. Результат: http://{schema_name}.localhost:8000/ - готовый магазин

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 19:13:10 +03:00

335 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
from django.core.exceptions import ValidationError
from accounts.models import CustomUser
from customers.models import Customer, Address
from products.models import Product, ProductKit
from shops.models import Shop
import uuid
class Order(models.Model):
"""
Заказ клиента для доставки цветов.
"""
# Основная информация
customer = models.ForeignKey(
Customer,
on_delete=models.PROTECT,
related_name='orders',
verbose_name="Клиент"
)
order_number = models.CharField(
max_length=50,
unique=True,
editable=False,
verbose_name="Номер заказа",
help_text="Уникальный номер заказа для отображения клиенту"
)
# Тип доставки
DELIVERY_TYPE_CHOICES = [
('courier', 'Курьерская доставка'),
('pickup', 'Самовывоз'),
]
delivery_type = models.CharField(
max_length=20,
choices=DELIVERY_TYPE_CHOICES,
default='courier',
verbose_name="Тип доставки"
)
# Адрес доставки (для курьерской доставки)
delivery_address = models.ForeignKey(
Address,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='orders',
verbose_name="Адрес доставки",
help_text="Обязательно для курьерской доставки"
)
# Пункт самовывоза (для самовывоза)
pickup_shop = models.ForeignKey(
Shop,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='pickup_orders',
verbose_name="Точка самовывоза",
help_text="Обязательно для самовывоза"
)
# Дата и время доставки/самовывоза
delivery_date = models.DateField(
verbose_name="Дата доставки/самовывоза"
)
delivery_time_start = models.TimeField(
verbose_name="Время от",
help_text="Начало временного интервала"
)
delivery_time_end = models.TimeField(
verbose_name="Время до",
help_text="Конец временного интервала"
)
delivery_cost = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
verbose_name="Стоимость доставки",
help_text="0 для самовывоза"
)
# Статус заказа
STATUS_CHOICES = [
('new', 'Новый'),
('confirmed', 'Подтвержден'),
('in_assembly', 'В сборке'),
('in_delivery', 'В доставке'),
('delivered', 'Доставлен'),
('cancelled', 'Отменен'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='new',
verbose_name="Статус заказа"
)
# Оплата
PAYMENT_METHOD_CHOICES = [
('cash_to_courier', 'Наличные курьеру'),
('card_to_courier', 'Карта курьеру'),
('online', 'Онлайн оплата'),
('bank_transfer', 'Банковский перевод'),
]
payment_method = models.CharField(
max_length=20,
choices=PAYMENT_METHOD_CHOICES,
default='cash_to_courier',
verbose_name="Способ оплаты"
)
is_paid = models.BooleanField(
default=False,
verbose_name="Оплачен"
)
total_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
verbose_name="Итоговая сумма заказа",
help_text="Общая сумма заказа включая доставку"
)
# Дополнительная информация
is_anonymous = models.BooleanField(
default=False,
verbose_name="Анонимная доставка",
help_text="Не сообщать получателю имя отправителя"
)
special_instructions = models.TextField(
blank=True,
null=True,
verbose_name="Особые пожелания",
help_text="Комментарии и пожелания к заказу"
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата создания"
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name="Дата обновления"
)
class Meta:
verbose_name = "Заказ"
verbose_name_plural = "Заказы"
indexes = [
models.Index(fields=['customer']),
models.Index(fields=['status']),
models.Index(fields=['delivery_date']),
models.Index(fields=['delivery_type']),
models.Index(fields=['created_at']),
models.Index(fields=['order_number']),
]
ordering = ['-created_at']
def __str__(self):
return f"Заказ #{self.order_number} - {self.customer}"
def save(self, *args, **kwargs):
# Генерируем уникальный номер заказа при создании
if not self.order_number:
self.order_number = self.generate_order_number()
super().save(*args, **kwargs)
def generate_order_number(self):
"""Генерирует уникальный номер заказа"""
# Формат: ORD-YYYYMMDD-XXXX (например: ORD-20250126-A3F2)
from datetime import datetime
date_str = datetime.now().strftime('%Y%m%d')
unique_id = uuid.uuid4().hex[:4].upper()
return f"ORD-{date_str}-{unique_id}"
def clean(self):
"""Валидация модели"""
super().clean()
# Проверка: для курьерской доставки обязателен адрес
if self.delivery_type == 'courier' and not self.delivery_address:
raise ValidationError({
'delivery_address': 'Для курьерской доставки необходимо указать адрес доставки'
})
# Проверка: для самовывоза обязателен пункт самовывоза
if self.delivery_type == 'pickup' and not self.pickup_shop:
raise ValidationError({
'pickup_shop': 'Для самовывоза необходимо выбрать точку самовывоза'
})
# Проверка: время окончания должно быть позже времени начала
if self.delivery_time_start and self.delivery_time_end:
if self.delivery_time_end <= self.delivery_time_start:
raise ValidationError({
'delivery_time_end': 'Время окончания должно быть позже времени начала'
})
def calculate_total(self):
"""Рассчитывает итоговую сумму заказа"""
items_total = sum(item.get_total_price() for item in self.items.all())
self.total_amount = items_total + self.delivery_cost
return self.total_amount
@property
def delivery_info(self):
"""Информация о доставке для отображения"""
if self.delivery_type == 'courier':
return f"Доставка по адресу: {self.delivery_address.full_address}"
elif self.delivery_type == 'pickup':
return f"Самовывоз из: {self.pickup_shop.name}"
return "Не указано"
@property
def delivery_time_window(self):
"""Временное окно доставки"""
return f"{self.delivery_time_start.strftime('%H:%M')} - {self.delivery_time_end.strftime('%H:%M')}"
class OrderItem(models.Model):
"""
Позиция (товар) в заказе.
Хранит информацию о товаре или комплекте, количестве и цене на момент заказа.
"""
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name='items',
verbose_name="Заказ"
)
# Товар или комплект (один из двух должен быть заполнен)
product = models.ForeignKey(
Product,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='order_items',
verbose_name="Товар"
)
product_kit = models.ForeignKey(
ProductKit,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='order_items',
verbose_name="Комплект товаров"
)
quantity = models.PositiveIntegerField(
default=1,
verbose_name="Количество"
)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Цена за единицу",
help_text="Цена на момент создания заказа (фиксируется)"
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата добавления"
)
class Meta:
verbose_name = "Позиция заказа"
verbose_name_plural = "Позиции заказа"
indexes = [
models.Index(fields=['order']),
models.Index(fields=['product']),
models.Index(fields=['product_kit']),
]
def __str__(self):
item_name = ""
if self.product:
item_name = self.product.name
elif self.product_kit:
item_name = self.product_kit.name
return f"{item_name} x{self.quantity} в заказе #{self.order.order_number}"
def clean(self):
"""Валидация модели"""
super().clean()
# Проверка: должен быть заполнен либо product, либо product_kit
if not self.product and not self.product_kit:
raise ValidationError(
'Необходимо указать либо товар, либо комплект товаров'
)
# Проверка: не должны быть заполнены оба поля одновременно
if self.product and self.product_kit:
raise ValidationError(
'Нельзя указать одновременно и товар, и комплект'
)
def save(self, *args, **kwargs):
# Автоматически фиксируем цену при создании, если она не указана
if not self.price:
if self.product:
self.price = self.product.price
elif self.product_kit:
self.price = self.product_kit.price
super().save(*args, **kwargs)
def get_total_price(self):
"""Возвращает общую стоимость позиции"""
return self.price * self.quantity
@property
def item_name(self):
"""Название товара/комплекта"""
if self.product:
return self.product.name
elif self.product_kit:
return self.product_kit.name
return "Не указано"