Files
octopus/myproject/customers/models.py
Andrey Smakotin fac3d55083 Удалена система лояльности из модели Customer
Удалены поля loyalty_tier, is_vip, get_loyalty_discount(), increment_total_spent().
Поле total_spent оставлено для будущего расчёта по заказам.
Обновлены admin, forms, views и шаблоны.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 17:05:18 +03:00

211 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, null=True, unique=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
total_spent = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
verbose_name="Общая сумма покупок"
)
# System customer flag
is_system_customer = models.BooleanField(
default=False,
db_index=True,
verbose_name="Системный клиент",
help_text="Автоматически созданный клиент для анонимных покупок и наличных продаж"
)
# 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']),
]
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
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):
# Защита системного клиента от изменений
if self.pk and self.is_system_customer:
# Получаем оригинальный объект из БД
try:
original = Customer.objects.get(pk=self.pk)
# Проверяем, не пытаются ли изменить критичные поля
if original.email != self.email:
raise ValidationError("Нельзя изменить email системного клиента")
if original.is_system_customer != self.is_system_customer:
raise ValidationError("Нельзя изменить флаг системного клиента")
except Customer.DoesNotExist:
pass
# Обеспечиваем нормализацию телефона, даже если 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 delete(self, *args, **kwargs):
"""Защита системного клиента от удаления"""
if self.is_system_customer:
raise ValidationError("Нельзя удалить системного клиента. Он необходим для работы системы.")
super().delete(*args, **kwargs)
@classmethod
def get_or_create_system_customer(cls):
"""
Получить или создать системного клиента для анонимных покупок.
Системный клиент используется для:
- Анонимных покупок в POS системе
- Покупок от неизвестных клиентов (проходимость)
- Наличных продаж без указания покупателя
Возвращает:
tuple: (customer, created) - объект клиента и флаг создания
"""
customer, created = cls.objects.get_or_create(
email="system@pos.customer",
defaults={
"name": "АНОНИМНЫЙ ПОКУПАТЕЛЬ (POS)",
"is_system_customer": True,
"notes": "SYSTEM_CUSTOMER - автоматически созданный клиент для анонимных покупок и наличных продаж",
}
)
return customer, created