diff --git a/myproject/customers/admin.py b/myproject/customers/admin.py index ec37b18..4f98903 100644 --- a/myproject/customers/admin.py +++ b/myproject/customers/admin.py @@ -21,6 +21,24 @@ class IsVipFilter(admin.SimpleListFilter): return queryset +class IsSystemCustomerFilter(admin.SimpleListFilter): + title = 'Системный клиент' + parameter_name = 'is_system_customer' + + def lookups(self, request, model_admin): + return ( + ('yes', 'Системный'), + ('no', 'Обычный'), + ) + + def queryset(self, request, queryset): + if self.value() == 'yes': + return queryset.filter(is_system_customer=True) + if self.value() == 'no': + return queryset.filter(is_system_customer=False) + return queryset + + @admin.register(Customer) class CustomerAdmin(admin.ModelAdmin): """Административный интерфейс для управления клиентами цветочного магазина""" @@ -31,11 +49,13 @@ class CustomerAdmin(admin.ModelAdmin): 'loyalty_tier', 'total_spent', 'is_vip', + 'is_system_customer', 'created_at' ) list_filter = ( 'loyalty_tier', IsVipFilter, + IsSystemCustomerFilter, 'created_at' ) search_fields = ( @@ -45,19 +65,45 @@ class CustomerAdmin(admin.ModelAdmin): ) date_hierarchy = 'created_at' ordering = ('-created_at',) - readonly_fields = ('created_at', 'updated_at', 'total_spent', 'is_vip') + readonly_fields = ('created_at', 'updated_at', 'total_spent', 'is_vip', 'is_system_customer') - fieldsets = ( ('Основная информация', { - 'fields': ('name', 'email', 'phone') + 'fields': ('name', 'email', 'phone', 'is_system_customer') }), ('Программа лояльности', { 'fields': ('loyalty_tier', 'total_spent', 'is_vip'), 'classes': ('collapse',) }), + ('Заметки', { + 'fields': ('notes',) + }), ('Даты', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) + + def get_readonly_fields(self, request, obj=None): + """Делаем все поля read-only для системного клиента""" + if obj and obj.is_system_customer: + # Для системного клиента все поля только для чтения + return ['name', 'email', 'phone', 'loyalty_tier', 'total_spent', 'is_vip', 'is_system_customer', 'notes', 'created_at', 'updated_at'] + return self.readonly_fields + + def has_delete_permission(self, request, obj=None): + """Запрет на удаление системного клиента""" + if obj and obj.is_system_customer: + return False + return super().has_delete_permission(request, obj) + + def changeform_view(self, request, object_id=None, form_url='', extra_context=None): + """Добавляем предупреждение для системного клиента""" + extra_context = extra_context or {} + if object_id: + obj = self.get_object(request, object_id) + if obj and obj.is_system_customer: + extra_context['readonly'] = True + from django.contrib import messages + messages.warning(request, 'Это системный клиент. Редактирование запрещено для обеспечения корректной работы системы.') + return super().changeform_view(request, object_id, form_url, extra_context) diff --git a/myproject/customers/forms.py b/myproject/customers/forms.py index f05f4a4..e0b6f98 100644 --- a/myproject/customers/forms.py +++ b/myproject/customers/forms.py @@ -15,6 +15,7 @@ class CustomerForm(forms.ModelForm): class Meta: model = Customer fields = ['name', 'email', 'phone', 'loyalty_tier', 'notes'] + exclude = ['is_system_customer'] widgets = { 'notes': forms.Textarea(attrs={'rows': 3}), } @@ -43,9 +44,9 @@ class CustomerForm(forms.ModelForm): """Проверяет уникальность email при создании/редактировании""" email = self.cleaned_data.get('email') - # Если email пустой, это нормально (blank=True) + # Нормализуем пустые значения в None (Django best practice для nullable полей) if not email: - return email + return None # Проверяем уникальность queryset = Customer.objects.filter(email=email) @@ -63,9 +64,9 @@ class CustomerForm(forms.ModelForm): """Проверяет уникальность телефона при создании/редактировании""" phone = self.cleaned_data.get('phone') - # Если телефон пустой, это нормально (blank=True) + # Нормализуем пустые значения в None (Django best practice для nullable полей) if not phone: - return phone + return None # Проверяем уникальность queryset = Customer.objects.filter(phone=phone) @@ -77,4 +78,17 @@ class CustomerForm(forms.ModelForm): if queryset.exists(): raise ValidationError('Клиент с таким номером телефона уже существует.') - return phone \ No newline at end of file + return phone + + def clean(self): + """Дополнительная валидация формы""" + cleaned_data = super().clean() + + # Защита от редактирования системного клиента + if self.instance and self.instance.pk and self.instance.is_system_customer: + raise ValidationError( + 'Системный клиент не может быть изменен. ' + 'Он необходим для корректной работы системы и создается автоматически.' + ) + + return cleaned_data \ No newline at end of file diff --git a/myproject/customers/migrations/0002_customer_is_system_customer.py b/myproject/customers/migrations/0002_customer_is_system_customer.py new file mode 100644 index 0000000..0c86c70 --- /dev/null +++ b/myproject/customers/migrations/0002_customer_is_system_customer.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.10 on 2025-11-19 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('customers', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='customer', + name='is_system_customer', + field=models.BooleanField(db_index=True, default=False, help_text='Автоматически созданный клиент для анонимных покупок и наличных продаж', verbose_name='Системный клиент'), + ), + ] diff --git a/myproject/customers/models.py b/myproject/customers/models.py index 9ea375b..444f651 100644 --- a/myproject/customers/models.py +++ b/myproject/customers/models.py @@ -41,15 +41,23 @@ class Customer(models.Model): ) total_spent = models.DecimalField( - max_digits=10, - decimal_places=2, + 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, + blank=True, null=True, verbose_name="Заметки", help_text="Заметки о клиенте, особые предпочтения и т.д." @@ -168,6 +176,19 @@ class Customer(models.Model): 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 вызывается непосредственно в модели, нам все равно нужно нормализовать @@ -189,6 +210,36 @@ class Customer(models.Model): 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, + "loyalty_tier": "no_discount", + "notes": "SYSTEM_CUSTOMER - автоматически созданный клиент для анонимных покупок и наличных продаж", + } + ) + return customer, created + def increment_total_spent(self, amount): """Увеличивает общую сумму покупок""" self.total_spent = self.total_spent + amount diff --git a/myproject/customers/templates/customers/customer_detail.html b/myproject/customers/templates/customers/customer_detail.html index 4600eec..2bb82fe 100644 --- a/myproject/customers/templates/customers/customer_detail.html +++ b/myproject/customers/templates/customers/customer_detail.html @@ -32,11 +32,11 @@
+ Используется для анонимных покупок и наличных продаж +
+ + Вернуться к списку + +