Remove direct reservation management from web interface

PROBLEM:
Direct deletion/creation of reservations via web interface bypassed
POS business logic, creating data inconsistencies (orphaned showcase kits,
incorrect stock calculations).

SOLUTION:
Make reservations read-only in web interface. All reservation management
now happens only through:
- POS (showcase kits)
- Orders module

CHANGES:
- Remove reservation-create and reservation-update URL routes
- Delete ReservationCreateView and ReservationUpdateView
- Remove ReservationForm (no longer needed)
- Delete reservation_form.html and reservation_update.html templates
- Update reservation_list.html to read-only view with info banner
- Add showcase and order columns to reservation list
- Clean up imports in urls.py and views/__init__.py

Reservations are now read-only for monitoring purposes only.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 13:17:04 +03:00
parent cd037e8f6b
commit 82261cbba7
7 changed files with 123 additions and 69 deletions

View File

@@ -104,24 +104,6 @@ class WriteOffForm(forms.ModelForm):
return cleaned_data
class ReservationForm(forms.ModelForm):
class Meta:
model = Reservation
fields = ['product', 'warehouse', 'quantity', 'order_item']
widgets = {
'product': forms.Select(attrs={'class': 'form-control'}),
'warehouse': forms.Select(attrs={'class': 'form-control'}),
'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
'order_item': forms.Select(attrs={'class': 'form-control'}),
}
def clean_quantity(self):
quantity = self.cleaned_data.get('quantity')
if quantity and quantity <= 0:
raise ValidationError('Количество должно быть больше нуля')
return quantity
class InventoryForm(forms.ModelForm):
class Meta:
model = Inventory

View File

@@ -1,7 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% block inventory_title %}Новое резервирование{% endblock %}
{% block breadcrumb_current %}Резервирования{% endblock %}
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Резервирование товара</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.product.label }} *</label>{{ form.product }}</div><div class="mb-3"><label class="form-label">{{ form.warehouse.label }} *</label>{{ form.warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.quantity.label }} *</label>{{ form.quantity|smart_quantity }}</div><div class="mb-3"><label class="form-label">{{ form.order_item.label }}</label>{{ form.order_item }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:reservation-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
<style>select,input{width:100%;}</style>
{% endblock %}

View File

@@ -1,6 +1,114 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% block inventory_title %}Резервирования{% endblock %}
{% block breadcrumb_current %}Резервирования{% endblock %}
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Активные резервирования <a href="{% url 'inventory:reservation-create' %}" class="btn btn-primary btn-sm float-end"><i class="bi bi-plus-circle"></i> Новое</a></h4></div><div class="card-body">{% if reservations %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Кол-во</th><th>Склад</th><th>Зарезервировано</th><th class="text-end">Действия</th></tr></thead><tbody>{% for r in reservations %}<tr><td>{{ r.product.name }}</td><td>{{ r.quantity|smart_quantity }}</td><td>{{ r.warehouse.name }}</td><td>{{ r.reserved_at|date:"d.m.Y" }}</td><td class="text-end"><a href="{% url 'inventory:reservation-update' r.pk %}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a></td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Резервирований не найдено.</div>{% endif %}</div></div>
{% block inventory_content %}
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">Активные резервирования</h4>
<span class="badge bg-info">Только для просмотра</span>
</div>
<div class="card-body">
<div class="alert alert-info mb-3">
<i class="bi bi-info-circle me-2"></i>
<strong>Информация:</strong> Резервы создаются автоматически через POS (витринные комплекты) и заказы.
Прямое управление резервами отключено для предотвращения несогласованности данных.
</div>
{% if reservations %}
<div class="table-responsive">
<table class="table table-hover table-sm align-middle">
<thead class="table-light">
<tr>
<th>Товар</th>
<th>Количество</th>
<th>Склад</th>
<th>Витрина</th>
<th>Заказ</th>
<th>Зарезервировано</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for r in reservations %}
<tr>
<td>
<strong>{{ r.product.name }}</strong>
</td>
<td>
<span class="badge bg-secondary">{{ r.quantity|smart_quantity }}</span>
</td>
<td>
<i class="bi bi-building me-1"></i>{{ r.warehouse.name }}
</td>
<td>
{% if r.showcase %}
<i class="bi bi-flower1 me-1"></i>{{ r.showcase.name }}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
{% if r.order_item %}
<a href="{% url 'orders:order-detail' r.order_item.order.id %}" class="text-decoration-none">
<i class="bi bi-receipt me-1"></i>Заказ #{{ r.order_item.order.id }}
</a>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
<small class="text-muted">{{ r.reserved_at|date:"d.m.Y H:i" }}</small>
</td>
<td>
<span class="badge bg-success">{{ r.get_status_display }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Пагинация -->
{% if is_paginated %}
<nav aria-label="Page navigation" class="mt-3">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">
Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-secondary text-center py-5">
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
<h5 class="mt-3">Активных резервирований не найдено</h5>
<p class="text-muted">Резервы создаются автоматически при оформлении заказов и витринных комплектов в POS</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% block inventory_title %}Изменение резервирования{% endblock %}
{% block breadcrumb_current %}Резервирования{% endblock %}
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Изменение статуса резервирования</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.status.label }}</label>{{ form.status }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:reservation-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
<style>select{width:100%;}</style>
{% endblock %}

View File

@@ -16,7 +16,7 @@ from .views import (
# Transfer
TransferListView, TransferBulkCreateView, TransferDetailView, TransferDeleteView, GetProductStockView,
# Reservation
ReservationListView, ReservationCreateView, ReservationUpdateView,
ReservationListView,
# Stock
StockListView, StockDetailView,
# StockBatch
@@ -79,10 +79,8 @@ urlpatterns = [
path('transfers/<int:pk>/delete/', TransferDeleteView.as_view(), name='transfer-delete'),
path('api/product-stock/', GetProductStockView.as_view(), name='api-product-stock'), # API для получения количества товара
# ==================== RESERVATION ====================
# ==================== RESERVATION (READ ONLY) ====================
path('reservations/', ReservationListView.as_view(), name='reservation-list'),
path('reservations/create/', ReservationCreateView.as_view(), name='reservation-create'),
path('reservations/<int:pk>/update-status/', ReservationUpdateView.as_view(), name='reservation-update'),
# ==================== STOCK (READ ONLY) ====================
path('stock/', StockListView.as_view(), name='stock-list'),

View File

@@ -9,7 +9,7 @@ Inventory Views Package
- inventory_ops.py: Инвентаризация и её строки
- writeoff.py: Списания товара
- transfer.py: Перемещения между складами
- reservation.py: Резервирования товара
- reservation.py: Резервирования товара (view-only)
- stock.py: Справочник остатков (view-only)
- batch.py: Справочник партий товара (view-only)
- allocation.py: Распределение продаж по партиям (view-only)
@@ -28,7 +28,7 @@ from .inventory_ops import (
)
from .writeoff import WriteOffListView, WriteOffCreateView, WriteOffUpdateView, WriteOffDeleteView
from .transfer import TransferListView, TransferBulkCreateView, TransferDetailView, TransferDeleteView, GetProductStockView
from .reservation import ReservationListView, ReservationCreateView, ReservationUpdateView
from .reservation import ReservationListView
from .stock import StockListView, StockDetailView
from .allocation import SaleBatchAllocationListView
from .movements import StockMovementListView
@@ -60,7 +60,7 @@ __all__ = [
# Transfer
'TransferListView', 'TransferBulkCreateView', 'TransferDetailView', 'TransferDeleteView', 'GetProductStockView',
# Reservation
'ReservationListView', 'ReservationCreateView', 'ReservationUpdateView',
'ReservationListView',
# Stock
'StockListView', 'StockDetailView',
# StockBatch

View File

@@ -1,46 +1,25 @@
# -*- coding: utf-8 -*-
"""
Reservation (Резервирование товара) views
GROUP 2: MEDIUM PRIORITY
Reservation (Резервирование товара) views - READ ONLY
Резервы управляются только через POS и Orders
"""
from django.views.generic import ListView, CreateView, UpdateView
from django.urls import reverse_lazy
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import Reservation
from ..forms import ReservationForm
class ReservationListView(LoginRequiredMixin, ListView):
"""
Список резервирований (только для просмотра).
Управление резервами происходит через POS (витринные комплекты) и Orders.
"""
model = Reservation
template_name = 'inventory/reservation/reservation_list.html'
context_object_name = 'reservations'
paginate_by = 20
def get_queryset(self):
"""Показываем все резервы со статусом 'reserved'"""
return Reservation.objects.filter(
status='reserved'
).select_related('product', 'warehouse', 'order_item').order_by('-reserved_at')
class ReservationCreateView(LoginRequiredMixin, CreateView):
model = Reservation
form_class = ReservationForm
template_name = 'inventory/reservation/reservation_form.html'
success_url = reverse_lazy('inventory:reservation-list')
def form_valid(self, form):
form.instance.status = 'reserved'
messages.success(self.request, f'Товар успешно зарезервирован.')
return super().form_valid(form)
class ReservationUpdateView(LoginRequiredMixin, UpdateView):
model = Reservation
fields = ['status']
template_name = 'inventory/reservation/reservation_update.html'
success_url = reverse_lazy('inventory:reservation-list')
def form_valid(self, form):
messages.success(self.request, f'Статус резервирования обновлен.')
return super().form_valid(form)
).select_related('product', 'warehouse', 'order_item', 'showcase').order_by('-reserved_at')