Добавлена отладочная страница для суперюзеров (Inventory Debug)

Реализация:
- Создан view debug_inventory_page (только для суперюзеров)
- URL: /inventory/debug/
- Компактный дизайн с минимальными отступами и маленьким шрифтом (10-11px)

Функционал:
1. Показывает полную картину инвентаризации на одной странице:
   - Заказы (Order) - номер, статус, покупатель, is_returned
   - Остатки (Stock) - доступно, зарезервировано, свободно
   - Партии (StockBatch) - количество, активность, дата поступления
   - Резервы (Reservation) - статус (reserved/converted_to_sale/released), заказ, даты
   - Продажи (Sale) - количество, цены, заказ
   - Списания (SaleBatchAllocation) - откуда списано, сколько

2. Фильтры:
   - По товару (dropdown с названием и SKU)
   - По номеру заказа (текстовое поле)
   - По складу (dropdown)
   - Кнопка 'Применить' и 'Сбросить'

3. UI:
   - Цветовая индикация статусов резервов
   - Бейджи для ключевых данных
   - Компактные таблицы Bootstrap
   - Неактивные партии выделены красным
   - Ограничение в 100 записей на таблицу для производительности

4. Навигация:
   - Ссылка 🔧 Debug в navbar (видна только суперюзерам)
   - Красный цвет для видимости

Юзкейс:
Суперюзер принимает товар на склад → оформляет заказ → меняет статусы →
переходит на /inventory/debug/ → видит полную картину всех изменений

Файлы:
- inventory/views/debug_views.py - новый view
- inventory/templates/inventory/debug_page.html - шаблон
- inventory/urls.py - добавлен роут
- templates/navbar.html - добавлена ссылка для суперюзеров
This commit is contained in:
2025-12-01 09:57:06 +03:00
parent 7b1922c186
commit 6bb15db5a0
4 changed files with 534 additions and 1 deletions

View File

@@ -0,0 +1,417 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Отладка Inventory - Суперюзер{% endblock %}
{% block extra_css %}
<style>
/* Компактный дизайн с маленькими отступами и шрифтом */
.debug-page {
font-size: 10px;
line-height: 1.3;
}
.debug-page h2 {
font-size: 14px;
margin-top: 15px;
margin-bottom: 8px;
font-weight: bold;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 3px;
}
.debug-page h3 {
font-size: 12px;
margin-top: 10px;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
.debug-page .table {
font-size: 10px;
margin-bottom: 8px;
}
.debug-page .table th {
padding: 3px 5px;
background-color: #f8f9fa;
font-weight: 600;
border: 1px solid #dee2e6;
}
.debug-page .table td {
padding: 2px 5px;
border: 1px solid #dee2e6;
vertical-align: middle;
}
.debug-page .badge {
font-size: 9px;
padding: 2px 5px;
}
.debug-page .filter-form {
background: #f8f9fa;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
}
.debug-page .filter-form .form-control,
.debug-page .filter-form .form-select {
font-size: 11px;
padding: 3px 8px;
height: auto;
}
.debug-page .filter-form label {
font-size: 11px;
margin-bottom: 2px;
font-weight: 600;
}
.debug-page .btn-sm {
font-size: 11px;
padding: 3px 10px;
}
.status-reserved { background-color: #fff3cd; }
.status-converted { background-color: #d1ecf1; }
.status-released { background-color: #d4edda; }
.inactive-row { background-color: #f8d7da; opacity: 0.7; }
.section-card {
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 8px;
margin-bottom: 10px;
background: white;
}
.summary-box {
display: inline-block;
padding: 3px 8px;
margin-right: 10px;
background: #e9ecef;
border-radius: 3px;
font-size: 10px;
}
.text-muted-small {
color: #6c757d;
font-size: 9px;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid debug-page mt-3">
<div class="row">
<div class="col-12">
<h2>🔧 Отладка Inventory (только для суперюзеров)</h2>
<!-- Фильтры -->
<div class="filter-form">
<form method="get" class="row g-2">
<div class="col-md-4">
<label for="product">Товар:</label>
<select name="product" id="product" class="form-select form-select-sm">
<option value="">-- Все товары --</option>
{% for prod in products %}
<option value="{{ prod.id }}" {% if product_id == prod.id|stringformat:"s" %}selected{% endif %}>
{{ prod.name }} ({{ prod.sku }})
</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="order">Номер заказа:</label>
<input type="text" name="order" id="order" class="form-control form-control-sm"
value="{{ order_number|default:'' }}" placeholder="ORD-100">
</div>
<div class="col-md-3">
<label for="warehouse">Склад:</label>
<select name="warehouse" id="warehouse" class="form-select form-select-sm">
<option value="">-- Все склады --</option>
{% for wh in warehouses %}
<option value="{{ wh.id }}" {% if warehouse_id == wh.id|stringformat:"s" %}selected{% endif %}>
{{ wh.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary btn-sm me-2">Применить</button>
<a href="{% url 'inventory:debug_page' %}" class="btn btn-secondary btn-sm">Сбросить</a>
</div>
</form>
</div>
{% if selected_product or selected_order or selected_warehouse %}
<div class="alert alert-info py-2" style="font-size: 11px;">
<strong>Активные фильтры:</strong>
{% if selected_product %}Товар: <strong>{{ selected_product.name }}</strong>{% endif %}
{% if selected_order %}Заказ: <strong>{{ selected_order.order_number }}</strong>{% endif %}
{% if selected_warehouse %}Склад: <strong>{{ selected_warehouse.name }}</strong>{% endif %}
</div>
{% endif %}
<!-- ЗАКАЗЫ -->
<div class="section-card">
<h3>📦 Заказы ({{ orders.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Номер</th>
<th>Статус</th>
<th>Возврат</th>
<th>Покупатель</th>
<th>Товары</th>
<th>Сумма</th>
<th>Создан</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order.id }}</td>
<td><strong>{{ order.order_number }}</strong></td>
<td>
<span class="badge bg-secondary">{{ order.status.name|default:"?" }}</span>
</td>
<td>
{% if order.is_returned %}
<span class="badge bg-warning">Возврат</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>{{ order.customer.name|default:"-" }}</td>
<td>{{ order.items.count }} шт</td>
<td>{{ order.total_price|floatformat:2 }}</td>
<td class="text-muted-small">{{ order.created_at|date:"d.m.Y H:i" }}</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center text-muted">Нет заказов</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- ОСТАТКИ (Stock) -->
<div class="section-card">
<h3>📊 Остатки Stock ({{ stocks.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Товар</th>
<th>Склад</th>
<th>Доступно</th>
<th>Зарезервировано</th>
<th>Свободно</th>
<th>Обновлено</th>
</tr>
</thead>
<tbody>
{% for stock in stocks %}
<tr>
<td>{{ stock.id }}</td>
<td><strong>{{ stock.product.name }}</strong></td>
<td>{{ stock.warehouse.name }}</td>
<td><span class="badge bg-primary">{{ stock.quantity_available }}</span></td>
<td><span class="badge bg-warning text-dark">{{ stock.quantity_reserved }}</span></td>
<td><span class="badge bg-success">{{ stock.quantity_free }}</span></td>
<td class="text-muted-small">{{ stock.updated_at|date:"d.m.Y H:i:s" }}</td>
</tr>
{% empty %}
<tr><td colspan="7" class="text-center text-muted">Нет данных Stock</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- ПАРТИИ (StockBatch) -->
<div class="section-card">
<h3>📦 Партии StockBatch ({{ stock_batches.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Товар</th>
<th>Склад</th>
<th>Кол-во</th>
<th>Себест.</th>
<th>Активна</th>
<th>Создана</th>
</tr>
</thead>
<tbody>
{% for batch in stock_batches %}
<tr {% if not batch.is_active %}class="inactive-row"{% endif %}>
<td>{{ batch.id }}</td>
<td><strong>{{ batch.product.name }}</strong></td>
<td>{{ batch.warehouse.name }}</td>
<td><span class="badge bg-info text-dark">{{ batch.quantity }}</span></td>
<td>{{ batch.cost_per_unit|floatformat:2 }}</td>
<td>
{% if batch.is_active %}
<span class="badge bg-success">Да</span>
{% else %}
<span class="badge bg-danger">Нет</span>
{% endif %}
</td>
<td class="text-muted-small">{{ batch.created_at|date:"d.m.Y H:i" }}</td>
</tr>
{% empty %}
<tr><td colspan="7" class="text-center text-muted">Нет партий</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- РЕЗЕРВЫ (Reservation) -->
<div class="section-card">
<h3>🔒 Резервы Reservation ({{ reservations.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Товар</th>
<th>Склад</th>
<th>Кол-во</th>
<th>Статус</th>
<th>Заказ</th>
<th>Создан</th>
<th>Преобразован</th>
<th>Освобожден</th>
</tr>
</thead>
<tbody>
{% for res in reservations %}
<tr class="
{% if res.status == 'reserved' %}status-reserved
{% elif res.status == 'converted_to_sale' %}status-converted
{% elif res.status == 'released' %}status-released
{% endif %}
">
<td>{{ res.id }}</td>
<td><strong>{{ res.product.name }}</strong></td>
<td>{{ res.warehouse.name }}</td>
<td><span class="badge bg-dark">{{ res.quantity }}</span></td>
<td>
{% if res.status == 'reserved' %}
<span class="badge bg-warning text-dark">Зарезервирован</span>
{% elif res.status == 'converted_to_sale' %}
<span class="badge bg-info">В продажу</span>
{% elif res.status == 'released' %}
<span class="badge bg-success">Освобожден</span>
{% else %}
<span class="badge bg-secondary">{{ res.status }}</span>
{% endif %}
</td>
<td>
{% if res.order_item.order %}
<strong>{{ res.order_item.order.order_number }}</strong>
{% else %}
-
{% endif %}
</td>
<td class="text-muted-small">{{ res.created_at|date:"d.m.Y H:i:s" }}</td>
<td class="text-muted-small">
{% if res.converted_at %}{{ res.converted_at|date:"d.m.Y H:i:s" }}{% else %}-{% endif %}
</td>
<td class="text-muted-small">
{% if res.released_at %}{{ res.released_at|date:"d.m.Y H:i:s" }}{% else %}-{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="9" class="text-center text-muted">Нет резервов</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- ПРОДАЖИ (Sale) -->
<div class="section-card">
<h3>💰 Продажи Sale ({{ sales.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Товар</th>
<th>Склад</th>
<th>Кол-во</th>
<th>Цена продажи</th>
<th>Себестоимость</th>
<th>Заказ</th>
<th>Документ</th>
<th>Создана</th>
</tr>
</thead>
<tbody>
{% for sale in sales %}
<tr>
<td>{{ sale.id }}</td>
<td><strong>{{ sale.product.name }}</strong></td>
<td>{{ sale.warehouse.name }}</td>
<td><span class="badge bg-primary">{{ sale.quantity }}</span></td>
<td>{{ sale.sale_price|floatformat:2 }}</td>
<td>{{ sale.cost_price|floatformat:2 }}</td>
<td>
{% if sale.order %}
<strong>{{ sale.order.order_number }}</strong>
{% else %}
-
{% endif %}
</td>
<td class="text-muted-small">{{ sale.document_number|default:"-" }}</td>
<td class="text-muted-small">{{ sale.created_at|date:"d.m.Y H:i:s" }}</td>
</tr>
{% empty %}
<tr><td colspan="9" class="text-center text-muted">Нет продаж</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- СПИСАНИЯ (SaleBatchAllocation) -->
<div class="section-card">
<h3>📤 Списания SaleBatchAllocation ({{ allocations.count }})</h3>
<div class="table-responsive">
<table class="table table-sm table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>Sale ID</th>
<th>Товар</th>
<th>Партия ID</th>
<th>Кол-во списано</th>
<th>Себест. за ед.</th>
</tr>
</thead>
<tbody>
{% for alloc in allocations %}
<tr>
<td>{{ alloc.id }}</td>
<td>{{ alloc.sale.id }}</td>
<td><strong>{{ alloc.sale.product.name }}</strong></td>
<td>{{ alloc.batch.id }}</td>
<td><span class="badge bg-danger">{{ alloc.quantity }}</span></td>
<td>{{ alloc.cost_per_unit|floatformat:2 }}</td>
</tr>
{% empty %}
<tr><td colspan="6" class="text-center text-muted">Нет списаний</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="alert alert-secondary py-2 mt-3" style="font-size: 10px;">
<strong>Примечание:</strong> Показаны последние 100 записей для каждой таблицы.
Используйте фильтры для уточнения результатов.
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -28,6 +28,8 @@ from .views import (
) )
# Showcase views # Showcase views
from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView, SetDefaultShowcaseView from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView, SetDefaultShowcaseView
# Debug views
from .views.debug_views import debug_inventory_page
from . import views from . import views
app_name = 'inventory' app_name = 'inventory'
@@ -102,4 +104,7 @@ urlpatterns = [
path('showcases/<int:pk>/edit/', ShowcaseUpdateView.as_view(), name='showcase-update'), path('showcases/<int:pk>/edit/', ShowcaseUpdateView.as_view(), name='showcase-update'),
path('showcases/<int:pk>/delete/', ShowcaseDeleteView.as_view(), name='showcase-delete'), path('showcases/<int:pk>/delete/', ShowcaseDeleteView.as_view(), name='showcase-delete'),
path('showcases/<int:pk>/set-default/', SetDefaultShowcaseView.as_view(), name='showcase-set-default'), path('showcases/<int:pk>/set-default/', SetDefaultShowcaseView.as_view(), name='showcase-set-default'),
# ==================== DEBUG (SUPERUSER ONLY) ====================
path('debug/', debug_inventory_page, name='debug_page'),
] ]

View File

@@ -0,0 +1,106 @@
"""
Отладочные view для суперюзеров.
Для мониторинга работы системы инвентаризации.
"""
from django.contrib.auth.decorators import login_required, user_passes_test
from django.shortcuts import render
from django.db.models import Q, Sum, Count
from inventory.models import StockBatch, Stock, Reservation, Sale, SaleBatchAllocation
from orders.models import Order
from products.models import Product
from inventory.models import Warehouse
def is_superuser(user):
"""Проверка что пользователь - суперюзер."""
return user.is_superuser
@login_required
@user_passes_test(is_superuser)
def debug_inventory_page(request):
"""
Отладочная страница для суперюзеров.
Показывает полную картину по инвентаризации: партии, остатки, резервы, продажи.
"""
# Получаем параметры фильтров
product_id = request.GET.get('product')
order_number = request.GET.get('order')
warehouse_id = request.GET.get('warehouse')
# Базовые querysets
stock_batches = StockBatch.objects.select_related('product', 'warehouse').order_by('-created_at')
stocks = Stock.objects.select_related('product', 'warehouse').order_by('product__name')
reservations = Reservation.objects.select_related(
'product', 'warehouse', 'order_item__order'
).order_by('-created_at')
sales = Sale.objects.select_related('product', 'warehouse', 'order').order_by('-created_at')
allocations = SaleBatchAllocation.objects.select_related(
'sale__product', 'batch'
).order_by('-id')
orders = Order.objects.prefetch_related('items').order_by('-created_at')
# Применяем фильтры
if product_id:
product = Product.objects.filter(id=product_id).first()
stock_batches = stock_batches.filter(product_id=product_id)
stocks = stocks.filter(product_id=product_id)
reservations = reservations.filter(product_id=product_id)
sales = sales.filter(product_id=product_id)
allocations = allocations.filter(sale__product_id=product_id)
orders = orders.filter(items__product_id=product_id).distinct()
else:
product = None
if order_number:
order = Order.objects.filter(order_number=order_number).first()
if order:
reservations = reservations.filter(order_item__order=order)
sales = sales.filter(order=order)
# Фильтруем товары по заказу
product_ids = order.items.values_list('product_id', flat=True)
stock_batches = stock_batches.filter(product_id__in=product_ids)
stocks = stocks.filter(product_id__in=product_ids)
allocations = allocations.filter(sale__order=order)
else:
order = None
if warehouse_id:
warehouse = Warehouse.objects.filter(id=warehouse_id).first()
stock_batches = stock_batches.filter(warehouse_id=warehouse_id)
stocks = stocks.filter(warehouse_id=warehouse_id)
reservations = reservations.filter(warehouse_id=warehouse_id)
sales = sales.filter(warehouse_id=warehouse_id)
else:
warehouse = None
# Ограничиваем количество записей для производительности
stock_batches = stock_batches[:100]
stocks = stocks[:100]
reservations = reservations[:100]
sales = sales[:100]
allocations = allocations[:100]
orders = orders[:50]
# Списки для фильтров
products = Product.objects.filter(is_active=True).order_by('name')[:200]
warehouses = Warehouse.objects.filter(is_active=True).order_by('name')
context = {
'stock_batches': stock_batches,
'stocks': stocks,
'reservations': reservations,
'sales': sales,
'allocations': allocations,
'orders': orders,
'products': products,
'warehouses': warehouses,
'selected_product': product,
'selected_order': order,
'selected_warehouse': warehouse,
'product_id': product_id,
'order_number': order_number,
'warehouse_id': warehouse_id,
}
return render(request, 'inventory/debug_page.html', context)

View File

@@ -51,8 +51,13 @@
<a class="nav-link {% if request.resolver_match.namespace == 'inventory' and 'showcase' in request.resolver_match.url_name %}active{% endif %}" href="{% url 'inventory:showcase-list' %}">Витрины</a> <a class="nav-link {% if request.resolver_match.namespace == 'inventory' and 'showcase' in request.resolver_match.url_name %}active{% endif %}" href="{% url 'inventory:showcase-list' %}">Витрины</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if request.resolver_match.namespace == 'inventory' and 'showcase' not in request.resolver_match.url_name %}active{% endif %}" href="{% url 'inventory:inventory-home' %}">Склад</a> <a class="nav-link {% if request.resolver_match.namespace == 'inventory' and 'showcase' not in request.resolver_match.url_name and 'debug' not in request.resolver_match.url_name %}active{% endif %}" href="{% url 'inventory:inventory-home' %}">Склад</a>
</li> </li>
{% if user.is_superuser %}
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'debug_page' %}active{% endif %}" href="{% url 'inventory:debug_page' %}" style="color: #dc3545; font-weight: bold;">🔧 Debug</a>
</li>
{% endif %}
{% endif %} {% endif %}
</ul> </ul>