diff --git a/myproject/customers/templates/customers/customer_detail.html b/myproject/customers/templates/customers/customer_detail.html
index f8c4207..005304b 100644
--- a/myproject/customers/templates/customers/customer_detail.html
+++ b/myproject/customers/templates/customers/customer_detail.html
@@ -345,8 +345,8 @@
Оплачено
{% elif order.status and order.status.is_negative_end and order.amount_paid > 0 %}
-
- Возврат
+
+ Возврат ({{ order.amount_paid|floatformat:2 }} руб.)
{% elif order.amount_paid > 0 %}
@@ -377,8 +377,8 @@
{% if order.status and order.status.is_negative_end and order.amount_paid > 0 %}
-
- {{ order.amount_paid|floatformat:2 }} руб.
+
+ {{ order.amount_paid|floatformat:2 }} руб.
{% else %}
—
diff --git a/myproject/orders/templates/orders/order_list.html b/myproject/orders/templates/orders/order_list.html
index 2d32d46..efedb18 100644
--- a/myproject/orders/templates/orders/order_list.html
+++ b/myproject/orders/templates/orders/order_list.html
@@ -105,7 +105,7 @@
{% for order in page_obj %}
-
+ 0 %}class="table-warning"{% endif %}>
|
{{ order.order_number }}
@@ -157,8 +157,8 @@
Оплачен
{% elif order.status and order.status.is_negative_end and order.amount_paid > 0 %}
-
- Возврат
+
+ Возврат ({{ order.amount_paid|floatformat:2 }} руб.)
{% elif order.amount_paid > 0 %}
diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py
index e57bb24..8070f3e 100644
--- a/myproject/products/models/kits.py
+++ b/myproject/products/models/kits.py
@@ -338,7 +338,7 @@ class KitItem(models.Model):
for priority in self.priorities.select_related('product').order_by('priority', 'id')
]
# Иначе возвращаем все товары из группы
- return list(self.variant_group.products.filter(is_active=True))
+ return list(self.variant_group.products.filter(status='active'))
return []
diff --git a/myproject/products/services/kit_pricing.py b/myproject/products/services/kit_pricing.py
index 47585d4..0ec98d0 100644
--- a/myproject/products/services/kit_pricing.py
+++ b/myproject/products/services/kit_pricing.py
@@ -113,7 +113,7 @@ class KitCostCalculator:
product = kit_item.product
if not product and kit_item.variant_group:
# Берем первый продукт из группы вариантов
- product = kit_item.variant_group.products.filter(is_active=True).first()
+ product = kit_item.variant_group.products.filter(status='active').first()
if product and product.cost_price:
item_cost = product.cost_price
@@ -163,7 +163,7 @@ class KitCostCalculator:
if not product and kit_item.variant_group:
# Берем первый активный продукт из группы вариантов
- product = kit_item.variant_group.products.filter(is_active=True).first()
+ product = kit_item.variant_group.products.filter(status='active').first()
if kit_item.variant_group:
product_name = f"[Варианты] {kit_item.variant_group.name}"
diff --git a/myproject/products/views/category_views.py b/myproject/products/views/category_views.py
index 257458a..c2b9cfe 100644
--- a/myproject/products/views/category_views.py
+++ b/myproject/products/views/category_views.py
@@ -112,13 +112,13 @@ class ProductCategoryListView(LoginRequiredMixin, ListView):
result.append(tree_item)
# 2. Добавляем активные товары этой категории (отсортированные по имени)
- products = category.products.filter(is_active=True).order_by('name')
+ products = category.products.filter(status='active').order_by('name')
for product in products:
product_item = TreeItem(product, 'product', depth + 1, category.pk)
result.append(product_item)
# 3. Добавляем активные наборы этой категории (отсортированные по имени)
- kits = category.kits.filter(is_active=True).order_by('name')
+ kits = category.kits.filter(status='active').order_by('name')
for kit in kits:
kit_item = TreeItem(kit, 'kit', depth + 1, category.pk)
result.append(kit_item)
@@ -234,8 +234,8 @@ class ProductCategoryDetailView(LoginRequiredMixin, DetailView):
# Получаем дочерние категории
context['children_categories'] = self.object.children.filter(is_active=True)
# Получаем товары в категории
- context['products'] = self.object.products.filter(is_active=True)[:20]
- context['products_count'] = self.object.products.filter(is_active=True).count()
+ context['products'] = self.object.products.filter(status='active')[:20]
+ context['products_count'] = self.object.products.filter(status='active').count()
return context
diff --git a/myproject/tenants/management/commands/clear_tenant_data.py b/myproject/tenants/management/commands/clear_tenant_data.py
new file mode 100644
index 0000000..8eccf0b
--- /dev/null
+++ b/myproject/tenants/management/commands/clear_tenant_data.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+"""
+Management команда для полной очистки данных тенанта без удаления схемы и самого тенанта.
+
+ВАЖНО: Это необратимая операция! Все данные тенанта будут удалены, но схема и запись в Client останутся.
+
+Использование:
+ # Очистка всех данных тенанта
+ python manage.py clear_tenant_data --schema=anatol --noinput
+
+ # С подтверждением
+ python manage.py clear_tenant_data --schema=anatol
+"""
+from django.core.management.base import BaseCommand
+from django.db import connection
+from django.apps import apps
+from django_tenants.utils import schema_context
+from tenants.models import Client
+
+
+class Command(BaseCommand):
+ help = 'Полная очистка всех данных в схеме тенанта без удаления схемы и Client'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--schema',
+ type=str,
+ required=True,
+ help='Имя схемы БД тенанта для очистки (пример: anatol)'
+ )
+ parser.add_argument(
+ '--noinput',
+ action='store_true',
+ help='Не запрашивать подтверждение (для автоматизации)'
+ )
+
+ def handle(self, *args, **options):
+ self.stdout.write(self.style.SUCCESS('\n=== Очистка данных тенанта ===\n'))
+
+ # Получаем параметры
+ schema_name = options.get('schema')
+ noinput = options.get('noinput', False)
+
+ # Проверяем что тенант существует
+ try:
+ tenant = Client.objects.get(schema_name=schema_name)
+ except Client.DoesNotExist:
+ self.stdout.write(self.style.ERROR(f'\nОШИБКА: Тенант со схемой "{schema_name}" не найден\n'))
+ return
+
+ # Получаем список всех таблиц в схеме (кроме служебных django)
+ tables = self._get_tenant_tables(schema_name)
+
+ # Показываем информацию о том что будет удалено
+ self.stdout.write('='*70)
+ self.stdout.write(self.style.WARNING('ВНИМАНИЕ! Следующие данные будут удалены:'))
+ self.stdout.write('='*70)
+ self.stdout.write(f'\n[Тенант]')
+ self.stdout.write(f' Название: {tenant.name}')
+ self.stdout.write(f' Схема: {tenant.schema_name}')
+ self.stdout.write(f' Владелец: {tenant.owner_email}')
+
+ self.stdout.write(f'\n[База данных]')
+ self.stdout.write(f' Схема: "{schema_name}" (НЕ будет удалена)')
+ self.stdout.write(f' Будет очищено таблиц: {len(tables)}')
+
+ if tables:
+ self.stdout.write(f'\n[Таблицы для очистки]')
+ for table in tables[:20]: # Показываем первые 20
+ self.stdout.write(f' - {table}')
+ if len(tables) > 20:
+ self.stdout.write(f' ... и ещё {len(tables) - 20} таблиц')
+
+ self.stdout.write(f'\n[Что НЕ будет удалено]')
+ self.stdout.write(f' ✓ Запись Client (тенант)')
+ self.stdout.write(f' ✓ Схема БД "{schema_name}"')
+ self.stdout.write(f' ✓ Домены тенанта')
+
+ self.stdout.write('\n' + '='*70)
+
+ # Запрашиваем подтверждение
+ if not noinput:
+ confirm = input(self.style.WARNING('\nВы уверены? Введите "yes" для подтверждения: '))
+ if confirm.lower() not in ['yes', 'да', 'y']:
+ self.stdout.write(self.style.ERROR('\nОтменено\n'))
+ return
+
+ # Начинаем очистку
+ self.stdout.write('\n' + '='*70)
+ self.stdout.write(self.style.SUCCESS('Начинаю очистку данных...'))
+ self.stdout.write('='*70 + '\n')
+
+ try:
+ deleted_count = self._clear_tenant_data(schema_name, tables)
+
+ # Итоговое сообщение
+ self.stdout.write('\n' + '='*70)
+ self.stdout.write(self.style.SUCCESS('УСПЕХ: Данные тенанта очищены!'))
+ self.stdout.write('='*70)
+ self.stdout.write(self.style.WARNING('\nИтоги очистки:'))
+ self.stdout.write(f' - Очищено таблиц: {deleted_count}')
+ self.stdout.write(f' - Схема "{schema_name}" сохранена')
+ self.stdout.write(f' - Тенант "{tenant.name}" сохранён')
+
+ # Предлагаем инициализировать системные данные
+ self._suggest_init_data(schema_name)
+
+ except Exception as e:
+ self.stdout.write(self.style.ERROR(f'\nОШИБКА при очистке данных: {e}'))
+ self.stdout.write(self.style.ERROR(' Данные могут быть частично удалены. Проверьте состояние БД.\n'))
+ raise
+
+ def _get_tenant_tables(self, schema_name):
+ """
+ Получает список всех таблиц в схеме тенанта (кроме служебных).
+ """
+ with connection.cursor() as cursor:
+ cursor.execute("""
+ SELECT table_name
+ FROM information_schema.tables
+ WHERE table_schema = %s
+ AND table_type = 'BASE TABLE'
+ AND table_name NOT IN (
+ 'django_migrations',
+ 'django_content_type',
+ 'django_admin_log',
+ 'django_session'
+ )
+ ORDER BY table_name
+ """, [schema_name])
+
+ return [row[0] for row in cursor.fetchall()]
+
+ def _clear_tenant_data(self, schema_name, tables):
+ """
+ Очищает все данные в таблицах тенанта.
+ Использует TRUNCATE CASCADE для удаления связанных данных.
+ """
+ deleted_count = 0
+
+ with connection.cursor() as cursor:
+ # Устанавливаем search_path на схему тенанта
+ cursor.execute(f'SET search_path TO {schema_name}')
+
+ # Отключаем триггеры и ограничения для быстрой очистки
+ self.stdout.write('[1] Отключение триггеров и ограничений...')
+ cursor.execute('SET session_replication_role = replica;')
+
+ # Очищаем каждую таблицу
+ self.stdout.write(f'[2] Очистка {len(tables)} таблиц...')
+ for i, table in enumerate(tables, 1):
+ try:
+ # Используем TRUNCATE CASCADE для удаления связанных данных
+ cursor.execute(f'TRUNCATE TABLE {table} CASCADE;')
+ deleted_count += 1
+
+ if i % 10 == 0:
+ self.stdout.write(f' Очищено таблиц: {i}/{len(tables)}')
+
+ except Exception as e:
+ self.stdout.write(
+ self.style.WARNING(f' Предупреждение: не удалось очистить таблицу {table}: {e}')
+ )
+
+ # Включаем триггеры и ограничения обратно
+ self.stdout.write('[3] Включение триггеров и ограничений...')
+ cursor.execute('SET session_replication_role = DEFAULT;')
+
+ # Сбрасываем последовательности (sequences) для ID
+ self.stdout.write('[4] Сброс последовательностей (ID sequences)...')
+ cursor.execute("""
+ SELECT 'SELECT SETVAL(' ||
+ quote_literal(quote_ident(sequence_schema) || '.' || quote_ident(sequence_name)) ||
+ ', 1, false);'
+ FROM information_schema.sequences
+ WHERE sequence_schema = %s;
+ """, [schema_name])
+
+ for row in cursor.fetchall():
+ try:
+ cursor.execute(row[0])
+ except Exception as e:
+ # Игнорируем ошибки при сбросе последовательностей
+ pass
+
+ self.stdout.write(self.style.SUCCESS(f' OK: Все данные очищены'))
+
+ return deleted_count
+
+ def _suggest_init_data(self, schema_name):
+ """
+ Предлагает пользователю инициализировать системные данные.
+ """
+ self.stdout.write('\n' + '-'*70)
+ self.stdout.write(self.style.WARNING('Рекомендация:'))
+ self.stdout.write('-'*70)
+ self.stdout.write('Для работы системы необходимы системные данные.')
+ self.stdout.write('Выполните команду для их создания:\n')
+ self.stdout.write(self.style.SUCCESS(f' python manage.py init_tenant_data --schema={schema_name}'))
+ self.stdout.write('')
diff --git a/myproject/tenants/management/commands/init_tenant_data.py b/myproject/tenants/management/commands/init_tenant_data.py
new file mode 100644
index 0000000..a743ff9
--- /dev/null
+++ b/myproject/tenants/management/commands/init_tenant_data.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+"""
+Management команда для инициализации всех системных данных тенанта.
+
+Создаёт:
+- Системного клиента (анонимный покупатель для POS)
+- Системные статусы заказов
+- Системные способы оплаты
+
+Использование:
+ # Инициализация для конкретного тенанта
+ python manage.py init_tenant_data --schema=anatol
+
+ # С флагом --reset для пересоздания данных
+ python manage.py init_tenant_data --schema=anatol --reset
+"""
+from django.core.management.base import BaseCommand
+from django.db import connection
+from django_tenants.utils import get_tenant_model, schema_context
+
+
+class Command(BaseCommand):
+ help = 'Инициализация всех системных данных тенанта (клиент, статусы, способы оплаты)'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--schema',
+ type=str,
+ required=True,
+ help='Имя схемы БД тенанта (пример: anatol)'
+ )
+ parser.add_argument(
+ '--reset',
+ action='store_true',
+ help='Удалить и пересоздать все системные данные'
+ )
+
+ def handle(self, *args, **options):
+ schema_name = options.get('schema')
+ reset = options.get('reset', False)
+
+ # Проверяем что тенант существует
+ Tenant = get_tenant_model()
+ try:
+ tenant = Tenant.objects.get(schema_name=schema_name)
+ except Tenant.DoesNotExist:
+ self.stdout.write(self.style.ERROR(f'\nОШИБКА: Тенант со схемой "{schema_name}" не найден\n'))
+ return
+
+ self.stdout.write(self.style.SUCCESS('\n=== Инициализация системных данных тенанта ===\n'))
+ self.stdout.write(f'Тенант: {tenant.name} ({schema_name})\n')
+
+ # Переключаемся на схему тенанта
+ with schema_context(schema_name):
+ # 1. Создаём системного клиента
+ self.stdout.write('\n' + '='*70)
+ self.stdout.write('[1] Создание системного клиента...')
+ self.stdout.write('='*70)
+
+ from customers.models import Customer
+
+ if reset:
+ # Удаляем существующего системного клиента
+ system_customers = Customer.objects.filter(email="system@pos.customer")
+ if system_customers.exists():
+ count = system_customers.count()
+ system_customers.delete()
+ self.stdout.write(self.style.WARNING(f' Удалено системных клиентов: {count}'))
+
+ try:
+ system_customer, created = Customer.get_or_create_system_customer()
+ if created:
+ self.stdout.write(self.style.SUCCESS(f' ✓ Системный клиент создан: {system_customer.name}'))
+ self.stdout.write(f' Email: {system_customer.email}')
+ self.stdout.write(f' ID: {system_customer.id}')
+ else:
+ self.stdout.write(self.style.WARNING(f' • Системный клиент уже существует: {system_customer.name}'))
+ self.stdout.write(f' ID: {system_customer.id}')
+ except Exception as e:
+ self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании системного клиента: {e}'))
+
+ # 2. Создаём системные статусы заказов
+ self.stdout.write('\n' + '='*70)
+ self.stdout.write('[2] Создание системных статусов заказов...')
+ self.stdout.write('='*70)
+
+ from orders.models import OrderStatus
+ from orders.services.order_status_service import OrderStatusService
+
+ if reset:
+ count = OrderStatus.objects.filter(is_system=True).count()
+ if count > 0:
+ OrderStatus.objects.filter(is_system=True).delete()
+ self.stdout.write(self.style.WARNING(f' Удалено системных статусов: {count}'))
+
+ try:
+ OrderStatusService.create_default_statuses()
+ statuses = OrderStatus.objects.filter(is_system=True).order_by('order')
+ self.stdout.write(self.style.SUCCESS(f' ✓ Создано системных статусов: {statuses.count()}'))
+
+ for status in statuses:
+ end_type = ''
+ if status.is_positive_end:
+ end_type = ' [Успешный]'
+ elif status.is_negative_end:
+ end_type = ' [Отрицательный]'
+ self.stdout.write(f' - {status.name:<20} ({status.code:<15}){end_type}')
+
+ except Exception as e:
+ self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании статусов: {e}'))
+
+ # 3. Создаём системные способы оплаты
+ self.stdout.write('\n' + '='*70)
+ self.stdout.write('[3] Создание системных способов оплаты...')
+ self.stdout.write('='*70)
+
+ from orders.models import PaymentMethod
+
+ if reset:
+ count = PaymentMethod.objects.filter(is_system=True).count()
+ if count > 0:
+ PaymentMethod.objects.filter(is_system=True).delete()
+ self.stdout.write(self.style.WARNING(f' Удалено системных способов оплаты: {count}'))
+
+ payment_methods = [
+ {
+ 'code': 'account_balance',
+ 'name': 'С баланса счёта',
+ 'description': 'Оплата из кошелька клиента',
+ 'is_system': True,
+ 'order': 0
+ },
+ {
+ 'code': 'cash',
+ 'name': 'Наличными',
+ 'description': 'Оплата наличными деньгами',
+ 'is_system': True,
+ 'order': 1
+ },
+ {
+ 'code': 'card',
+ 'name': 'Картой',
+ 'description': 'Оплата банковской картой',
+ 'is_system': True,
+ 'order': 2
+ },
+ {
+ 'code': 'online',
+ 'name': 'Онлайн',
+ 'description': 'Онлайн оплата через платежную систему',
+ 'is_system': True,
+ 'order': 3
+ },
+ {
+ 'code': 'legal_entity',
+ 'name': 'Безнал от ЮРЛИЦ',
+ 'description': 'Безналичный расчёт от юридических лиц',
+ 'is_system': True,
+ 'order': 4
+ },
+ ]
+
+ created_count = 0
+ try:
+ for method_data in payment_methods:
+ method, created = PaymentMethod.objects.get_or_create(
+ code=method_data['code'],
+ defaults=method_data
+ )
+ if created:
+ created_count += 1
+ self.stdout.write(self.style.SUCCESS(f' ✓ Создан способ оплаты: {method.name}'))
+ else:
+ self.stdout.write(self.style.WARNING(f' • Уже существует: {method.name}'))
+
+ self.stdout.write(self.style.SUCCESS(f'\n Готово! Создано {created_count} новых способов оплаты.'))
+
+ except Exception as e:
+ self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании способов оплаты: {e}'))
+
+ # Итоговое сообщение
+ self.stdout.write('\n' + '='*70)
+ self.stdout.write(self.style.SUCCESS('УСПЕХ: Инициализация системных данных завершена!'))
+ self.stdout.write('='*70)
+ self.stdout.write('\nТенант готов к работе.\n')
| |