Files
octopus/myproject/customers/models.py

289 lines
11 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.
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