from django.db import models from django.core.exceptions import ValidationError from accounts.models import CustomUser from customers.models import Customer from products.models import Product, ProductKit from shops.models import Shop from simple_history.models import HistoricalRecords class OrderStatus(models.Model): """ Статус заказа, управляется отдельно для каждого тенанта. Благодаря django-tenants в TENANT_APPS, данные изолированы по схемам. """ name = models.CharField( max_length=100, verbose_name="Название статуса" ) code = models.SlugField( unique=True, verbose_name="Код статуса", help_text="Уникальный идентификатор (например: 'completed', 'cancelled')" ) label = models.CharField( max_length=100, verbose_name="Метка для отображения", blank=True ) is_system = models.BooleanField( default=False, verbose_name="Системный статус", help_text="True для встроенных статусов (draft, completed, cancelled)" ) is_positive_end = models.BooleanField( default=False, verbose_name="Положительный конец", help_text="True если это финальный успешный статус (Выполнен)" ) is_negative_end = models.BooleanField( default=False, verbose_name="Отрицательный конец", help_text="True если это финальный отрицательный статус (Отменен)" ) order = models.PositiveIntegerField( default=0, verbose_name="Порядок отображения" ) color = models.CharField( max_length=7, blank=True, default='#808080', verbose_name="Цвет (hex)", help_text="Например: #FF5733" ) description = models.TextField( blank=True, verbose_name="Описание" ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) created_by = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_order_statuses', verbose_name="Создано" ) updated_by = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name='updated_order_statuses', verbose_name="Последнее изменение" ) class Meta: verbose_name = "Статус заказа" verbose_name_plural = "Статусы заказов" ordering = ['order', 'name'] indexes = [ models.Index(fields=['code']), models.Index(fields=['is_system']), models.Index(fields=['order']), ] def __str__(self): return self.name @property def orders_count(self): """Количество заказов в этом статусе""" return self.orders.count() class Address(models.Model): """ Модель адреса доставки для заказа цветочного магазина в Минске. Адрес принадлежит конкретному заказу доставки. """ # Информация о получателе recipient_name = models.CharField( max_length=200, blank=True, null=True, verbose_name="Имя получателя", help_text="Имя человека, которому будет доставлен заказ" ) recipient_phone = models.CharField( max_length=20, blank=True, null=True, verbose_name="Телефон получателя", help_text="Контактный телефон получателя для уточнения адреса" ) street = models.CharField( max_length=255, blank=True, null=True, verbose_name="Улица" ) building_number = models.CharField( max_length=20, blank=True, null=True, verbose_name="Номер здания" ) apartment_number = models.CharField( max_length=20, blank=True, null=True, verbose_name="Номер квартиры/офиса" ) entrance = models.CharField( max_length=20, blank=True, null=True, verbose_name="Подъезд", help_text="Номер подъезда/входа" ) floor = models.CharField( max_length=20, blank=True, null=True, verbose_name="Этаж" ) intercom_code = models.CharField( max_length=100, blank=True, null=True, verbose_name="Код домофона", help_text="Код домофона для входа в здание" ) # Дополнительная информация для доставки delivery_instructions = models.TextField( blank=True, null=True, verbose_name="Инструкции для доставки", help_text="Дополнительные инструкции для курьера" ) confirm_address_with_recipient = models.BooleanField( default=False, 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=['created_at']), ] ordering = ['-created_at'] def __str__(self): # Собираем компоненты адреса address_parts = [] if self.street: address_parts.append(self.street) if self.building_number: address_parts.append(self.building_number) if self.apartment_number: address_parts.append(f"кв/офис {self.apartment_number}") address_line = ", ".join(address_parts) if address_parts else "Адрес не указан" # Формируем строку с именем получателя if self.recipient_name: return f"{self.recipient_name} - {address_line}" return address_line @property def full_address(self): """Полный адрес для доставки""" # Собираем основные компоненты адреса address_parts = [] if self.street: address_parts.append(self.street) if self.building_number: address_parts.append(self.building_number) # Если нет основных данных, возвращаем сообщение if not address_parts: return "Адрес не указан" address = ", ".join(address_parts) # Добавляем квартиру/офис if self.apartment_number: address += f", кв/офис {self.apartment_number}" # Собираем дополнительные детали details = [] if self.entrance: details.append(f"подъезд {self.entrance}") if self.floor: details.append(f"этаж {self.floor}") if details: address += f" ({', '.join(details)})" return address class Order(models.Model): """ Заказ клиента для доставки цветов. """ # Основная информация customer = models.ForeignKey( Customer, on_delete=models.PROTECT, related_name='orders', verbose_name="Клиент" ) order_number = models.PositiveIntegerField( unique=True, editable=False, verbose_name="Номер заказа", help_text="Уникальный номер заказа" ) # Тип доставки is_delivery = models.BooleanField( default=True, verbose_name="С доставкой", help_text="True - доставка курьером, False - самовывоз" ) # Адрес доставки (для курьерской доставки) delivery_address = models.OneToOneField( Address, on_delete=models.CASCADE, null=True, blank=True, related_name='order', 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( null=True, blank=True, verbose_name="Дата доставки/самовывоза", help_text="Может быть заполнено позже" ) delivery_time_start = models.TimeField( null=True, blank=True, verbose_name="Время от", help_text="Начало временного интервала" ) delivery_time_end = models.TimeField( null=True, blank=True, verbose_name="Время до", help_text="Конец временного интервала" ) delivery_cost = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Стоимость доставки", help_text="0 для самовывоза" ) is_custom_delivery_cost = models.BooleanField( default=False, verbose_name="Стоимость доставки установлена вручную", help_text="True если стоимость доставки была изменена вручную" ) # Статус заказа status = models.ForeignKey( 'OrderStatus', on_delete=models.PROTECT, related_name='orders', null=True, blank=True, verbose_name="Статус заказа" ) # Флаг для отслеживания возвратов is_returned = models.BooleanField( default=False, verbose_name="Возвращен", help_text="True если заказ был выполнен, но потом отменен или возвращен клиентом" ) # Автосохранение (для черновиков) last_autosave_at = models.DateTimeField( null=True, blank=True, verbose_name="Последнее автосохранение", help_text="Время последнего автоматического сохранения черновика" ) # Оплата 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="Общая сумма заказа включая доставку" ) # Скидки discount_amount = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Сумма скидки", help_text="Применяется вручную или через систему скидок" ) # Частичная оплата amount_paid = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Оплачено", help_text="Сумма, внесенная клиентом" ) PAYMENT_STATUS_CHOICES = [ ('unpaid', 'Не оплачен'), ('partial', 'Частично оплачен'), ('paid', 'Оплачен полностью'), ] payment_status = models.CharField( max_length=20, choices=PAYMENT_STATUS_CHOICES, default='unpaid', verbose_name="Статус оплаты", help_text="Обновляется автоматически при добавлении платежей" ) # Дополнительная информация customer_is_recipient = models.BooleanField( default=True, verbose_name="Покупатель является получателем", help_text="Если отмечено, данные получателя не требуются отдельно" ) # Данные получателя (если покупатель != получатель) recipient_name = models.CharField( max_length=200, blank=True, null=True, verbose_name="Имя получателя", help_text="Заполняется, если покупатель не является получателем" ) recipient_phone = models.CharField( max_length=20, blank=True, null=True, 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="Дата обновления" ) modified_by = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name='modified_orders', verbose_name="Изменен пользователем", help_text="Последний пользователь, изменивший заказ" ) # История изменений history = HistoricalRecords() class Meta: verbose_name = "Заказ" verbose_name_plural = "Заказы" indexes = [ models.Index(fields=['customer']), models.Index(fields=['status']), models.Index(fields=['delivery_date']), models.Index(fields=['is_delivery']), models.Index(fields=['payment_status']), models.Index(fields=['created_at']), models.Index(fields=['order_number']), models.Index(fields=['is_custom_delivery_cost']), ] ordering = ['-created_at'] def __str__(self): return f"Заказ #{self.order_number} - {self.customer}" def save(self, *args, **kwargs): # Генерируем уникальный номер заказа при создании if not self.order_number: last_order = Order.objects.order_by('-order_number').first() self.order_number = (last_order.order_number if last_order else 0) + 1 super().save(*args, **kwargs) def clean(self): """Валидация модели""" super().clean() # Проверка: для доставки обязателен адрес if self.is_delivery and not self.delivery_address: raise ValidationError({ 'delivery_address': 'Для доставки необходимо указать адрес доставки' }) # Проверка: для самовывоза обязателен пункт самовывоза if not self.is_delivery 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 get_delivery_cost(self): """ Возвращает стоимость доставки: - Если установлена вручную - использует ручное значение - Если автоматическая - вычисляет на основе правил Returns: Decimal: Стоимость доставки """ if self.is_custom_delivery_cost: return self.delivery_cost else: from orders.services.delivery_cost_calculator import DeliveryCostCalculator return DeliveryCostCalculator.calculate(self) def set_delivery_cost(self, cost, is_custom=True): """ Устанавливает стоимость доставки. Args: cost: Новая стоимость доставки (Decimal) is_custom: True если устанавливается вручную, False если автоматически """ self.delivery_cost = cost self.is_custom_delivery_cost = is_custom def reset_delivery_cost(self): """ Сбрасывает стоимость доставки на автоматический расчет. """ from orders.services.delivery_cost_calculator import DeliveryCostCalculator self.delivery_cost = DeliveryCostCalculator.calculate(self) self.is_custom_delivery_cost = False def recalculate_delivery_cost(self): """ Пересчитывает стоимость доставки, если она не установлена вручную. Используется при изменении параметров заказа (товаров, адреса и т.д.) """ if not self.is_custom_delivery_cost: from orders.services.delivery_cost_calculator import DeliveryCostCalculator self.delivery_cost = DeliveryCostCalculator.calculate(self) def calculate_total(self): """Рассчитывает итоговую сумму заказа""" items_total = sum(item.get_total_price() for item in self.items.all()) # Пересчитываем стоимость доставки если она автоматическая self.recalculate_delivery_cost() subtotal = items_total + self.delivery_cost self.total_amount = subtotal - self.discount_amount return self.total_amount def update_payment_status(self): """Автоматически обновляет статус оплаты на основе amount_paid""" if self.amount_paid >= self.total_amount: self.payment_status = 'paid' self.is_paid = True elif self.amount_paid > 0: self.payment_status = 'partial' self.is_paid = False else: self.payment_status = 'unpaid' self.is_paid = False self.save() def is_draft(self): """Проверяет, является ли заказ черновиком""" return self.status and self.status.code == 'draft' @property def amount_due(self): """Остаток к оплате""" return max(self.total_amount - self.amount_paid, 0) @property def delivery_cost_display(self): """ Возвращает строку для отображения стоимости доставки с пометкой. Полезно в админке и шаблонах. """ cost = self.get_delivery_cost() suffix = " (ручная)" if self.is_custom_delivery_cost else " (авто)" return f"{cost} руб.{suffix}" @property def delivery_info(self): """Информация о доставке для отображения""" if self.is_delivery: if self.delivery_address: return f"Доставка по адресу: {self.delivery_address.full_address}" return "Доставка (адрес не указан)" else: if self.pickup_shop: return f"Самовывоз из: {self.pickup_shop.name}" return "Самовывоз (точка не указана)" @property def delivery_time_window(self): """Временное окно доставки""" if self.delivery_time_start and self.delivery_time_end: return f"{self.delivery_time_start.strftime('%H:%M')} - {self.delivery_time_end.strftime('%H:%M')}" return "Время не указано" 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="Цена на момент создания заказа (фиксируется)" ) is_custom_price = models.BooleanField( default=False, verbose_name="Цена изменена вручную", help_text="True если цена была изменена вручную при создании заказа" ) # Временные метки 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 "Не указано" @property def original_price(self): """Оригинальная цена товара/комплекта из каталога""" if self.product: return self.product.actual_price elif self.product_kit: return self.product_kit.actual_price return None @property def price_difference(self): """Разница между установленной ценой и оригинальной""" if self.is_custom_price and self.original_price: return self.price - self.original_price return None class Payment(models.Model): """ Платеж по заказу. Хранит историю всех платежей, включая частичные оплаты. """ order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name='payments', verbose_name="Заказ" ) amount = models.DecimalField( max_digits=10, decimal_places=2, verbose_name="Сумма платежа" ) payment_method = models.CharField( max_length=20, choices=Order.PAYMENT_METHOD_CHOICES, verbose_name="Способ оплаты" ) payment_date = models.DateTimeField( auto_now_add=True, verbose_name="Дата и время платежа" ) created_by = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name='payments_created', verbose_name="Принял платеж" ) notes = models.TextField( blank=True, null=True, verbose_name="Примечания", help_text="Дополнительная информация о платеже" ) class Meta: verbose_name = "Платеж" verbose_name_plural = "Платежи" ordering = ['-payment_date'] indexes = [ models.Index(fields=['order']), models.Index(fields=['payment_date']), ] def __str__(self): return f"Платеж {self.amount} руб. по заказу #{self.order.order_number}" def save(self, *args, **kwargs): """При сохранении платежа обновляем сумму оплаты в заказе""" super().save(*args, **kwargs) # Пересчитываем общую сумму оплаты в заказе self.order.amount_paid = sum(p.amount for p in self.order.payments.all()) self.order.update_payment_status()