Add full CRUD interface for Showcase management
Implemented complete web interface for managing showcases (display areas for ready-made bouquets) with: **Backend:** - ShowcaseForm with validation (unique name per warehouse) - ShowcaseListView with filtering by warehouse and status - ShowcaseCreateView, ShowcaseUpdateView with success messages - ShowcaseDeleteView with active reservation validation - URL routes: list, create, edit, delete **Frontend:** - List page with warehouse grouping, active reservations count - Responsive table with filters (warehouse, status) - Create/edit form with Bootstrap styling - Delete confirmation with active reservations check - Breadcrumb navigation **Features:** ✅ One warehouse can have multiple showcases (ForeignKey relationship) ✅ Unique showcase names within each warehouse ✅ Display active reservation counts for each showcase ✅ Prevent deletion if showcase has active reservations ✅ Auto-select default warehouse when creating showcase ✅ Navigation link added to main navbar between "Касса" and "Склад" ✅ Active state highlighting in navigation **Files created:** - inventory/forms_showcase.py (ShowcaseForm) - inventory/views/showcase.py (4 CBV views) - inventory/templates/inventory/showcase/ (list, form, delete templates) **Files modified:** - inventory/urls.py (added showcase routes) - inventory/forms.py (added Showcase import) - templates/navbar.html (added "Витрины" link) URL structure: /inventory/showcases/ - list /inventory/showcases/create/ - create /inventory/showcases/<id>/edit/ - edit /inventory/showcases/<id>/delete/ - delete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Warehouse, Incoming, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch,
|
Warehouse, Incoming, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch,
|
||||||
TransferBatch, TransferItem
|
TransferBatch, TransferItem, Showcase
|
||||||
)
|
)
|
||||||
from products.models import Product
|
from products.models import Product
|
||||||
|
|
||||||
@@ -460,3 +460,4 @@ class TransferBulkForm(forms.Form):
|
|||||||
raise ValidationError('Склад-источник и склад-назначение должны быть разными')
|
raise ValidationError('Склад-источник и склад-назначение должны быть разными')
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|||||||
73
myproject/inventory/forms_showcase.py
Normal file
73
myproject/inventory/forms_showcase.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from .models import Showcase, Warehouse
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Форма для создания и редактирования витрин.
|
||||||
|
Витрина привязывается к складу и используется для выкладки готовых букетов.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = Showcase
|
||||||
|
fields = ['name', 'warehouse', 'description', 'is_active']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Название витрины (например: Витрина №1, Витрина у входа)'
|
||||||
|
}),
|
||||||
|
'warehouse': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'description': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 3,
|
||||||
|
'placeholder': 'Описание витрины, её расположение или особенности'
|
||||||
|
}),
|
||||||
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
|
}
|
||||||
|
labels = {
|
||||||
|
'name': 'Название витрины',
|
||||||
|
'warehouse': 'Склад',
|
||||||
|
'description': 'Описание',
|
||||||
|
'is_active': 'Активна',
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'warehouse': 'Склад, к которому привязана витрина',
|
||||||
|
'is_active': 'Неактивные витрины скрыты из списка выбора',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# Фильтруем только активные склады
|
||||||
|
self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True).order_by('name')
|
||||||
|
|
||||||
|
# Если создаём новую витрину и есть склад по умолчанию - предвыбираем его
|
||||||
|
if not self.instance.pk and not self.initial.get('warehouse'):
|
||||||
|
default_warehouse = Warehouse.objects.filter(
|
||||||
|
is_active=True,
|
||||||
|
is_default=True
|
||||||
|
).first()
|
||||||
|
if default_warehouse:
|
||||||
|
self.initial['warehouse'] = default_warehouse.id
|
||||||
|
|
||||||
|
def clean_name(self):
|
||||||
|
"""Проверка уникальности названия витрины в рамках склада"""
|
||||||
|
name = self.cleaned_data.get('name')
|
||||||
|
warehouse = self.cleaned_data.get('warehouse')
|
||||||
|
|
||||||
|
if name and warehouse:
|
||||||
|
# Проверяем уникальность названия в рамках склада
|
||||||
|
queryset = Showcase.objects.filter(name=name, warehouse=warehouse)
|
||||||
|
|
||||||
|
# При редактировании исключаем текущий экземпляр
|
||||||
|
if self.instance and self.instance.pk:
|
||||||
|
queryset = queryset.exclude(pk=self.instance.pk)
|
||||||
|
|
||||||
|
if queryset.exists():
|
||||||
|
raise ValidationError(
|
||||||
|
f'Витрина с названием "{name}" уже существует на складе "{warehouse.name}". '
|
||||||
|
'Пожалуйста, выберите другое название.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return name
|
||||||
135
myproject/inventory/templates/inventory/showcase/delete.html
Normal file
135
myproject/inventory/templates/inventory/showcase/delete.html
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1 class="mb-2 text-danger">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>Удаление витрины
|
||||||
|
</h1>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'inventory:inventory-home' %}">Склад</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'inventory:showcase-list' %}">Витрины</a></li>
|
||||||
|
<li class="breadcrumb-item active">Удаление</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>Подтверждение удаления
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="lead">
|
||||||
|
Вы действительно хотите удалить витрину <strong>"{{ object.name }}"</strong>
|
||||||
|
на складе <strong>"{{ object.warehouse.name }}"</strong>?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if has_active_reservations %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-x-circle me-2"></i>Невозможно удалить витрину
|
||||||
|
</h6>
|
||||||
|
<p class="mb-2">
|
||||||
|
На этой витрине есть <strong>{{ active_reservations.count }}</strong> активных резервов.
|
||||||
|
Сначала необходимо освободить или продать зарезервированные товары.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if active_reservations %}
|
||||||
|
<hr>
|
||||||
|
<p class="mb-2"><strong>Активные резервы:</strong></p>
|
||||||
|
<ul class="mb-0">
|
||||||
|
{% for reservation in active_reservations|slice:":5" %}
|
||||||
|
<li>{{ reservation.product.name }} — {{ reservation.quantity }} шт.</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if active_reservations.count > 5 %}
|
||||||
|
<li class="text-muted">...и еще {{ active_reservations.count|add:"-5" }} резервов</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'inventory:showcase-list' %}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-arrow-left me-1"></i>Вернуться к списку
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'inventory:reservation-list' %}?showcase={{ object.id }}" class="btn btn-info">
|
||||||
|
<i class="bi bi-eye me-1"></i>Просмотреть резервы
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="bi bi-info-circle me-2"></i>
|
||||||
|
<strong>Внимание:</strong> Это действие нельзя отменить. Все данные о витрине будут удалены безвозвратно.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="bi bi-trash me-1"></i>Да, удалить витрину
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'inventory:showcase-list' %}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Отмена
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Card -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0">Информация о витрине</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-6">Название:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">{{ object.name }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Склад:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">{{ object.warehouse.name }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Статус:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">
|
||||||
|
{% if object.is_active %}
|
||||||
|
<span class="badge bg-success">Активна</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Неактивна</span>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Создана:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">{{ object.created_at|date:"d.m.Y H:i" }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Обновлена:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">{{ object.updated_at|date:"d.m.Y H:i" }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Активные резервы:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">
|
||||||
|
{% if has_active_reservations %}
|
||||||
|
<span class="badge bg-danger">{{ active_reservations.count }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
151
myproject/inventory/templates/inventory/showcase/form.html
Normal file
151
myproject/inventory/templates/inventory/showcase/form.html
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1 class="mb-2">
|
||||||
|
<i class="bi bi-flower1 text-primary me-2"></i>{{ form_title }}
|
||||||
|
</h1>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'inventory:inventory-home' %}">Склад</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'inventory:showcase-list' %}">Витрины</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ form_title }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Name -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.name.id_for_label }}" class="form-label fw-semibold">
|
||||||
|
{{ form.name.label }}
|
||||||
|
<span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.name }}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.name.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.name.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Warehouse -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.warehouse.id_for_label }}" class="form-label fw-semibold">
|
||||||
|
{{ form.warehouse.label }}
|
||||||
|
<span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.warehouse }}
|
||||||
|
{% if form.warehouse.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.warehouse.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.warehouse.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.warehouse.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.description.id_for_label }}" class="form-label fw-semibold">
|
||||||
|
{{ form.description.label }}
|
||||||
|
</label>
|
||||||
|
{{ form.description }}
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.description.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.description.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Is Active -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.is_active }}
|
||||||
|
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
|
||||||
|
{{ form.is_active.label }}
|
||||||
|
</label>
|
||||||
|
{% if form.is_active.help_text %}
|
||||||
|
<div><small class="form-text text-muted">{{ form.is_active.help_text }}</small></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if form.is_active.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{{ form.is_active.errors }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-check-circle me-1"></i>{{ submit_text }}
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'inventory:showcase-list' %}" class="btn btn-secondary">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Отмена
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Card (for edit mode) -->
|
||||||
|
{% if object %}
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0">Информация о витрине</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-6">Создана:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">{{ object.created_at|date:"d.m.Y H:i" }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-6">Обновлена:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">{{ object.updated_at|date:"d.m.Y H:i" }}</dd>
|
||||||
|
|
||||||
|
{% if active_reservations_count is not None %}
|
||||||
|
<dt class="col-sm-6">Активные резервы:</dt>
|
||||||
|
<dd class="col-sm-6 text-end">
|
||||||
|
{% if active_reservations_count > 0 %}
|
||||||
|
<span class="badge bg-info">{{ active_reservations_count }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
162
myproject/inventory/templates/inventory/showcase/list.html
Normal file
162
myproject/inventory/templates/inventory/showcase/list.html
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-2">
|
||||||
|
<i class="bi bi-flower1 text-primary me-2"></i>Витрины
|
||||||
|
</h1>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'inventory:inventory-home' %}">Склад</a></li>
|
||||||
|
<li class="breadcrumb-item active">Витрины</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'inventory:showcase-create' %}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>Создать витрину
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get" class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Склад</label>
|
||||||
|
<select name="warehouse" class="form-select">
|
||||||
|
<option value="">Все склады</option>
|
||||||
|
{% for warehouse in warehouses %}
|
||||||
|
<option value="{{ warehouse.id }}" {% if request.GET.warehouse == warehouse.id|stringformat:"s" %}selected{% endif %}>
|
||||||
|
{{ warehouse.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Статус</label>
|
||||||
|
<select name="is_active" class="form-select">
|
||||||
|
<option value="">Все</option>
|
||||||
|
<option value="1" {% if request.GET.is_active == "1" %}selected{% endif %}>Только активные</option>
|
||||||
|
<option value="0" {% if request.GET.is_active == "0" %}selected{% endif %}>Только неактивные</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-secondary me-2">
|
||||||
|
<i class="bi bi-funnel me-1"></i>Применить
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'inventory:showcase-list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>Сбросить
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Showcases List -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
{% if showcases %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Склад</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th class="text-center">Активные резервы</th>
|
||||||
|
<th class="text-center">Статус</th>
|
||||||
|
<th class="text-center">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% regroup showcases by warehouse as showcases_by_warehouse %}
|
||||||
|
{% for warehouse_group in showcases_by_warehouse %}
|
||||||
|
<!-- Warehouse Header Row -->
|
||||||
|
<tr class="table-secondary">
|
||||||
|
<td colspan="6" class="fw-bold">
|
||||||
|
<i class="bi bi-building me-2"></i>{{ warehouse_group.grouper.name }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Showcases in this warehouse -->
|
||||||
|
{% for showcase in warehouse_group.list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ showcase.name }}</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-muted">{{ showcase.warehouse.name }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if showcase.description %}
|
||||||
|
<small class="text-muted">{{ showcase.description|truncatewords:10 }}</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted fst-italic">—</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if showcase.active_reservations_count > 0 %}
|
||||||
|
<span class="badge bg-info">{{ showcase.active_reservations_count }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if showcase.is_active %}
|
||||||
|
<span class="badge bg-success">Активна</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Неактивна</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{% url 'inventory:showcase-update' showcase.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
title="Редактировать">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'inventory:showcase-delete' showcase.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-danger ms-1"
|
||||||
|
title="Удалить">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="bi bi-flower1 text-muted" style="font-size: 4rem;"></i>
|
||||||
|
<h4 class="mt-3 text-muted">Витрин не найдено</h4>
|
||||||
|
<p class="text-muted">Создайте первую витрину для выкладки букетов</p>
|
||||||
|
<a href="{% url 'inventory:showcase-create' %}" class="btn btn-primary mt-2">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>Создать витрину
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -26,6 +26,8 @@ from .views import (
|
|||||||
# StockMovement
|
# StockMovement
|
||||||
StockMovementListView,
|
StockMovementListView,
|
||||||
)
|
)
|
||||||
|
# Showcase views
|
||||||
|
from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'inventory'
|
app_name = 'inventory'
|
||||||
@@ -95,4 +97,10 @@ urlpatterns = [
|
|||||||
|
|
||||||
# ==================== MOVEMENT (READ ONLY) ====================
|
# ==================== MOVEMENT (READ ONLY) ====================
|
||||||
path('movements/', StockMovementListView.as_view(), name='movement-list'),
|
path('movements/', StockMovementListView.as_view(), name='movement-list'),
|
||||||
|
|
||||||
|
# ==================== SHOWCASE ====================
|
||||||
|
path('showcases/', ShowcaseListView.as_view(), name='showcase-list'),
|
||||||
|
path('showcases/create/', ShowcaseCreateView.as_view(), name='showcase-create'),
|
||||||
|
path('showcases/<int:pk>/edit/', ShowcaseUpdateView.as_view(), name='showcase-update'),
|
||||||
|
path('showcases/<int:pk>/delete/', ShowcaseDeleteView.as_view(), name='showcase-delete'),
|
||||||
]
|
]
|
||||||
|
|||||||
175
myproject/inventory/views/showcase.py
Normal file
175
myproject/inventory/views/showcase.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from inventory.models import Showcase, Reservation
|
||||||
|
from inventory.forms_showcase import ShowcaseForm
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class ShowcaseListView(ListView):
|
||||||
|
"""
|
||||||
|
Список всех витрин с группировкой по складам.
|
||||||
|
Отображает информацию о количестве активных резервов на каждой витрине.
|
||||||
|
"""
|
||||||
|
model = Showcase
|
||||||
|
template_name = 'inventory/showcase/list.html'
|
||||||
|
context_object_name = 'showcases'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
Получаем витрины с аннотацией количества активных резервов.
|
||||||
|
Сортируем по складу и названию.
|
||||||
|
"""
|
||||||
|
queryset = Showcase.objects.select_related('warehouse').annotate(
|
||||||
|
active_reservations_count=Count(
|
||||||
|
'reservations',
|
||||||
|
filter=Q(reservations__status='reserved')
|
||||||
|
)
|
||||||
|
).order_by('warehouse__name', 'name')
|
||||||
|
|
||||||
|
# Фильтрация по складу, если указан GET-параметр
|
||||||
|
warehouse_id = self.request.GET.get('warehouse')
|
||||||
|
if warehouse_id:
|
||||||
|
queryset = queryset.filter(warehouse_id=warehouse_id)
|
||||||
|
|
||||||
|
# Фильтрация по статусу активности
|
||||||
|
is_active = self.request.GET.get('is_active')
|
||||||
|
if is_active == '1':
|
||||||
|
queryset = queryset.filter(is_active=True)
|
||||||
|
elif is_active == '0':
|
||||||
|
queryset = queryset.filter(is_active=False)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['title'] = 'Витрины'
|
||||||
|
|
||||||
|
# Добавляем информацию для фильтров
|
||||||
|
from inventory.models import Warehouse
|
||||||
|
context['warehouses'] = Warehouse.objects.filter(is_active=True).order_by('name')
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class ShowcaseCreateView(CreateView):
|
||||||
|
"""
|
||||||
|
Создание новой витрины.
|
||||||
|
"""
|
||||||
|
model = Showcase
|
||||||
|
form_class = ShowcaseForm
|
||||||
|
template_name = 'inventory/showcase/form.html'
|
||||||
|
success_url = reverse_lazy('inventory:showcase-list')
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Сохраняем витрину и показываем сообщение об успехе"""
|
||||||
|
response = super().form_valid(form)
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
f'Витрина "{self.object.name}" успешно создана на складе "{self.object.warehouse.name}"'
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['title'] = 'Создание витрины'
|
||||||
|
context['form_title'] = 'Новая витрина'
|
||||||
|
context['submit_text'] = 'Создать витрину'
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class ShowcaseUpdateView(UpdateView):
|
||||||
|
"""
|
||||||
|
Редактирование существующей витрины.
|
||||||
|
"""
|
||||||
|
model = Showcase
|
||||||
|
form_class = ShowcaseForm
|
||||||
|
template_name = 'inventory/showcase/form.html'
|
||||||
|
success_url = reverse_lazy('inventory:showcase-list')
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Сохраняем изменения и показываем сообщение об успехе"""
|
||||||
|
response = super().form_valid(form)
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
f'Витрина "{self.object.name}" успешно обновлена'
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['title'] = f'Редактирование витрины: {self.object.name}'
|
||||||
|
context['form_title'] = f'Редактирование: {self.object.name}'
|
||||||
|
context['submit_text'] = 'Сохранить изменения'
|
||||||
|
|
||||||
|
# Добавляем информацию о резервах на витрине
|
||||||
|
context['active_reservations_count'] = Reservation.objects.filter(
|
||||||
|
showcase=self.object,
|
||||||
|
status='reserved'
|
||||||
|
).count()
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(login_required, name='dispatch')
|
||||||
|
class ShowcaseDeleteView(DeleteView):
|
||||||
|
"""
|
||||||
|
Удаление витрины с подтверждением.
|
||||||
|
Проверяет наличие активных резервов перед удалением.
|
||||||
|
"""
|
||||||
|
model = Showcase
|
||||||
|
template_name = 'inventory/showcase/delete.html'
|
||||||
|
success_url = reverse_lazy('inventory:showcase-list')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['title'] = f'Удаление витрины: {self.object.name}'
|
||||||
|
|
||||||
|
# Проверяем наличие активных резервов
|
||||||
|
active_reservations = Reservation.objects.filter(
|
||||||
|
showcase=self.object,
|
||||||
|
status='reserved'
|
||||||
|
).select_related('product')
|
||||||
|
|
||||||
|
context['active_reservations'] = active_reservations
|
||||||
|
context['has_active_reservations'] = active_reservations.exists()
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
"""Проверяем наличие активных резервов перед удалением"""
|
||||||
|
showcase = self.get_object()
|
||||||
|
|
||||||
|
# Проверка активных резервов
|
||||||
|
active_reservations_count = Reservation.objects.filter(
|
||||||
|
showcase=showcase,
|
||||||
|
status='reserved'
|
||||||
|
).count()
|
||||||
|
|
||||||
|
if active_reservations_count > 0:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
f'Невозможно удалить витрину "{showcase.name}": '
|
||||||
|
f'на ней есть {active_reservations_count} активных резервов. '
|
||||||
|
'Сначала освободите или продайте зарезервированные товары.'
|
||||||
|
)
|
||||||
|
return redirect('inventory:showcase-delete', pk=showcase.pk)
|
||||||
|
|
||||||
|
# Удаляем витрину
|
||||||
|
showcase_name = showcase.name
|
||||||
|
response = super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f'Витрина "{showcase_name}" успешно удалена'
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -45,7 +45,10 @@
|
|||||||
<a class="nav-link {% if request.resolver_match.namespace == 'pos' %}active{% endif %}" href="{% url 'pos:terminal' %}">Касса</a>
|
<a class="nav-link {% if request.resolver_match.namespace == 'pos' %}active{% endif %}" href="{% url 'pos:terminal' %}">Касса</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.resolver_match.namespace == 'inventory' %}active{% endif %}" href="{% url 'inventory:inventory-home' %}">Склад</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 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>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user