import phonenumbers from django.core.exceptions import ValidationError from django.db import models from phonenumber_field.modelfields import PhoneNumberField from products.models import Product class Customer(models.Model): """ Модель клиента для цветочного магазина в Минске, Беларусь. """ # Name field that is not required to be unique name = models.CharField(max_length=200, blank=True, verbose_name="Имя") email = models.EmailField(blank=True, verbose_name="Email") # Phone with validation using django-phonenumber-field phone = PhoneNumberField( blank=True, null=True, unique=True, verbose_name="Телефон", help_text="Введите телефон в любом формате, например: +375291234567, 80255270546, 8(029)1234567 - будет автоматически преобразован в международный формат" ) # Temporary field to store raw phone number during initialization _raw_phone = None # Loyalty program loyalty_tier = models.CharField( max_length=20, choices=[ ('no_discount', 'Без скидки'), ('bronze', 'Бронза'), ('silver', 'Серебро'), ('gold', 'Золото'), ('platinum', 'Платина'), ], default='no_discount', verbose_name="Уровень лояльности" ) total_spent = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Общая сумма покупок" ) # Additional notes notes = models.TextField( blank=True, null=True, verbose_name="Заметки", help_text="Заметки о клиенте, особые предпочтения и т.д." ) # Timestamps 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=['name']), models.Index(fields=['email']), models.Index(fields=['phone']), models.Index(fields=['created_at']), models.Index(fields=['loyalty_tier']), ] ordering = ['-created_at'] def __str__(self): if self.name: return self.name if self.phone: return str(self.phone) if self.email: return self.email return "Безымянный клиент" @property def full_name(self): """Полное имя клиента""" return self.name @property def is_vip(self): """Проверяет, является ли клиент VIP на основе уровня лояльности""" return self.loyalty_tier in ("gold", "platinum") def get_loyalty_discount(self): """Возвращает скидку в зависимости от уровня лояльности""" discounts = { 'no_discount': 0, 'bronze': 0, 'silver': 5, # 5% 'gold': 10, # 10% 'platinum': 15 # 15% } return discounts.get(self.loyalty_tier, 0) def validate_unique(self, exclude=None): """Переопределение для корректной проверки уникальности телефона при обновлениях""" # Снова нормализуем номер телефона перед проверкой уникальности if self.phone: # Проверяем существующих клиентов с таким же телефоном (исключая текущий экземпляр при обновлении) existing = Customer.objects.filter(phone=self.phone) if self.pk: existing = existing.exclude(pk=self.pk) if existing.exists(): raise ValidationError({'phone': 'Пользователь с таким номером телефона уже существует.'}) # Вызываем родительский validate_unique для обработки других проверок super().validate_unique(exclude=exclude) def clean_phone(self): """Пользовательская очистка поля телефона для нормализации перед валидацией.""" if self.phone: try: # Парсим номер телефона, чтобы проверить его валидность и нормализовать raw_phone = str(self.phone) # Если начинается с '8' и имеет 11 цифр, это может быть российский формат if raw_phone.startswith('8') and len(raw_phone) == 11: # Сначала пробуем BY для белорусских номеров parsed = phonenumbers.parse(raw_phone, "BY") if phonenumbers.is_valid_number(parsed): return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) # Если BY не работает, пробуем RU как запасной вариант parsed = phonenumbers.parse(raw_phone, "RU") if phonenumbers.is_valid_number(parsed): return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) # Пробуем парсить без кода страны (может быть уже в международном формате) parsed = phonenumbers.parse(raw_phone, None) if phonenumbers.is_valid_number(parsed): return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) except phonenumbers.NumberParseException: # Если парсинг не удался, возвращаем как есть и позволяем валидации поля обработать это pass return self.phone def clean_fields(self, exclude=None): # Нормализуем телефон перед запуском проверки полей if not exclude: exclude = [] if 'phone' not in exclude and self.phone: normalized = None try: normalized = self.clean_phone() except Exception: normalized = None if normalized: # присваиваем нормализованное значение (E.164), чтобы PhoneNumberField видел корректный формат self.phone = normalized super().clean_fields(exclude=exclude) def clean(self): """Дополнительная валидация при необходимости.""" super().clean() def save(self, *args, **kwargs): # Обеспечиваем нормализацию телефона, даже если save вызывается напрямую (не через форму) # На данный момент, если вызов прошел через валидацию формы, телефон уже должен быть нормализован # Но если save вызывается непосредственно в модели, нам все равно нужно нормализовать if self.phone and str(self.phone).startswith('8') and len(str(self.phone)) == 11: # Это, вероятно, номер в формате страны, который требует нормализации try: # Сначала пробуем BY для белорусских номеров parsed = phonenumbers.parse(str(self.phone), "BY") if phonenumbers.is_valid_number(parsed): self.phone = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) else: # Если BY не работает, пробуем RU как запасной вариант parsed = phonenumbers.parse(str(self.phone), "RU") if phonenumbers.is_valid_number(parsed): self.phone = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) except phonenumbers.NumberParseException: # Если парсинг не удался, не меняем и позволяем валидации поля обработать это pass super().save(*args, **kwargs) def increment_total_spent(self, amount): """Увеличивает общую сумму покупок""" self.total_spent = self.total_spent + amount self.save(update_fields=['total_spent'])