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): """Override to handle unique phone validation properly during updates""" # Run the phone number normalization again before unique validation if self.phone: # Check for existing customers with the same phone (excluding current instance if updating) existing = Customer.objects.filter(phone=self.phone) if self.pk: existing = existing.exclude(pk=self.pk) if existing.exists(): raise ValidationError({'phone': 'Пользователь с таким номером телефона уже существует.'}) # Call parent validate_unique to handle other validation super().validate_unique(exclude=exclude) def clean_phone(self): """Custom cleaning for phone field to normalize before validation.""" if self.phone: try: # Parse the phone number to check if it's valid and normalize it raw_phone = str(self.phone) # If it starts with '8' and has 11 digits, it might be Russian domestic format if raw_phone.startswith('8') and len(raw_phone) == 11: # Try BY first for Belarusian numbers parsed = phonenumbers.parse(raw_phone, "BY") if phonenumbers.is_valid_number(parsed): return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) # If BY doesn't work, try RU as fallback parsed = phonenumbers.parse(raw_phone, "RU") if phonenumbers.is_valid_number(parsed): return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) # Try to parse without country code (might already be in international format) parsed = phonenumbers.parse(raw_phone, None) if phonenumbers.is_valid_number(parsed): return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) except phonenumbers.NumberParseException: # If parsing fails, return as is and let field validation handle it pass return self.phone def clean_fields(self, exclude=None): # Normalize phone before field validation runs 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: # assign normalized value (E.164) so PhoneNumberField sees корректный формат self.phone = normalized super().clean_fields(exclude=exclude) def clean(self): """Additional validation if needed.""" super().clean() def save(self, *args, **kwargs): # Ensure phone is normalized even if save is called directly (not through form) # At this point, if it came through form validation, phone should already be normalized # But if save is called directly on the model, we still need to normalize if self.phone and str(self.phone).startswith('8') and len(str(self.phone)) == 11: # This is likely a domestic format number that needs normalization try: # Try BY first for Belarusian numbers parsed = phonenumbers.parse(str(self.phone), "BY") if phonenumbers.is_valid_number(parsed): self.phone = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) else: # If BY doesn't work, try RU as fallback 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: # If parsing fails, don't change it and let the field validation handle it pass super().save(*args, **kwargs) def increment_total_spent(self, amount): """Увеличивает общую сумму покупок""" self.total_spent = self.total_spent + amount self.save(update_fields=['total_spent']) class Address(models.Model): """ Модель адреса доставки для клиентов цветочного магазина в Минске. Клиент может иметь несколько адресов для разных получателей. """ customer = models.ForeignKey( Customer, on_delete=models.CASCADE, related_name='addresses', verbose_name="Клиент" ) # Address information for delivery in Minsk recipient_name = models.CharField( max_length=200, verbose_name="Имя получателя", help_text="Имя человека, которому будет доставлен заказ" ) street = models.CharField( max_length=255, verbose_name="Улица" ) building_number = models.CharField( max_length=20, verbose_name="Номер здания" ) apartment_number = models.CharField( max_length=20, blank=True, null=True, verbose_name="Номер квартиры/офиса" ) district = models.CharField( max_length=100, blank=True, null=True, verbose_name="Район", help_text="Район в Минске для удобства доставки" ) # Additional information for delivery delivery_instructions = models.TextField( blank=True, null=True, verbose_name="Инструкции для доставки", help_text="Дополнительные инструкции для курьера (домофон, подъезд и т.д.)" ) is_default = models.BooleanField( default=False, 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=['customer']), models.Index(fields=['is_default']), models.Index(fields=['district']), ] ordering = ['-is_default', '-created_at'] def save(self, *args, **kwargs): if self.is_default: # If this address is being set as default, unset the default flag on other addresses for this customer Address.objects.filter(customer=self.customer, is_default=True).update(is_default=False) super().save(*args, **kwargs) def __str__(self): address_line = f"{self.street}, {self.building_number}" if self.apartment_number: address_line += f", кв/офис {self.apartment_number}" return f"{self.recipient_name} - {address_line}, {self.customer.full_name}" @property def full_address(self): """Полный адрес для доставки""" address = f"{self.street}, {self.building_number}" if self.apartment_number: address += f", кв/офис {self.apartment_number}" return address