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:
2025-11-20 10:52:53 +03:00
parent 07c8819936
commit 766ca3c87c
8 changed files with 710 additions and 2 deletions

View 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 %}

View 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 %}

View 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 %}