Обновили шапку и вывод всехтоваров. Добавили фильтры
This commit is contained in:
288
myproject/customers/models.py
Normal file
288
myproject/customers/models.py
Normal file
@@ -0,0 +1,288 @@
|
||||
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
|
||||
Reference in New Issue
Block a user