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 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 InventoryForm(forms.ModelForm):
class Meta: class Meta:
model = Inventory 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' %} {% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %} {% load inventory_filters %}
{% block inventory_title %}Резервирования{% endblock %} {% block inventory_title %}Резервирования{% endblock %}
{% block breadcrumb_current %}Резервирования{% 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 %} {% 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 # Transfer
TransferListView, TransferBulkCreateView, TransferDetailView, TransferDeleteView, GetProductStockView, TransferListView, TransferBulkCreateView, TransferDetailView, TransferDeleteView, GetProductStockView,
# Reservation # Reservation
ReservationListView, ReservationCreateView, ReservationUpdateView, ReservationListView,
# Stock # Stock
StockListView, StockDetailView, StockListView, StockDetailView,
# StockBatch # StockBatch
@@ -79,10 +79,8 @@ urlpatterns = [
path('transfers/<int:pk>/delete/', TransferDeleteView.as_view(), name='transfer-delete'), path('transfers/<int:pk>/delete/', TransferDeleteView.as_view(), name='transfer-delete'),
path('api/product-stock/', GetProductStockView.as_view(), name='api-product-stock'), # API для получения количества товара 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/', 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) ==================== # ==================== STOCK (READ ONLY) ====================
path('stock/', StockListView.as_view(), name='stock-list'), path('stock/', StockListView.as_view(), name='stock-list'),

View File

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

View File

@@ -1,46 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Reservation (Резервирование товара) views Reservation (Резервирование товара) views - READ ONLY
GROUP 2: MEDIUM PRIORITY Резервы управляются только через POS и Orders
""" """
from django.views.generic import ListView, CreateView, UpdateView from django.views.generic import ListView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import Reservation from ..models import Reservation
from ..forms import ReservationForm
class ReservationListView(LoginRequiredMixin, ListView): class ReservationListView(LoginRequiredMixin, ListView):
"""
Список резервирований (только для просмотра).
Управление резервами происходит через POS (витринные комплекты) и Orders.
"""
model = Reservation model = Reservation
template_name = 'inventory/reservation/reservation_list.html' template_name = 'inventory/reservation/reservation_list.html'
context_object_name = 'reservations' context_object_name = 'reservations'
paginate_by = 20 paginate_by = 20
def get_queryset(self): def get_queryset(self):
"""Показываем все резервы со статусом 'reserved'"""
return Reservation.objects.filter( return Reservation.objects.filter(
status='reserved' status='reserved'
).select_related('product', 'warehouse', 'order_item').order_by('-reserved_at') ).select_related('product', 'warehouse', 'order_item', 'showcase').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)