Implement flexible order status management system

Features:
- Created OrderStatus model for managing statuses per tenant
- Added system-level statuses: draft, new, confirmed, in_assembly, in_delivery, completed, return, cancelled
- Implemented CRUD views for managing order statuses
- Created OrderStatusService with status transitions and business logic hooks
- Updated Order model to use ForeignKey to OrderStatus
- Added is_returned flag for tracking returned orders
- Updated filters to work with new OrderStatus model
- Created management command for status initialization
- Added HTML templates for status list, form, and confirmation
- Fixed views.py to use OrderStatus instead of removed STATUS_CHOICES

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-13 16:29:50 +03:00
parent 0d5f0d2015
commit c7875f147c
28 changed files with 1337 additions and 390 deletions

View File

@@ -0,0 +1,129 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Удалить статус{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-8">
<h1>Удалить статус</h1>
</div>
<div class="col-md-4 text-end">
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Вернуться к статусам
</a>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">
<i class="fas fa-exclamation-triangle"></i> Подтвердите удаление
</h5>
</div>
<div class="card-body">
<div class="alert alert-warning mb-4">
<i class="fas fa-exclamation-circle"></i>
<strong>Внимание!</strong> Это действие необратимо.
</div>
<p>Вы собираетесь удалить статус:</p>
<div class="mb-3">
<div class="card">
<div class="card-body">
<h5>
<span style="display: inline-block; width: 20px; height: 20px; background-color: {{ object.color }}; border-radius: 3px; margin-right: 10px; vertical-align: middle;"></span>
{{ object.name }}
</h5>
<p class="text-muted mb-2">
<strong>Код:</strong> <code>{{ object.code }}</code>
</p>
{% if object.description %}
<p class="mb-0">
<strong>Описание:</strong><br>
{{ object.description }}
</p>
{% endif %}
</div>
</div>
</div>
{% if orders_count > 0 %}
<div class="alert alert-danger">
<i class="fas fa-ban"></i>
<strong>Невозможно удалить!</strong>
В этом статусе находится {{ orders_count }} заказ(ов).
Пожалуйста, измените статус этих заказов перед удалением.
</div>
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Вернуться к статусам
</a>
{% else %}
<form method="post" class="mt-4">
{% csrf_token %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash"></i> Да, удалить статус
</button>
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-times"></i> Отменить
</a>
</div>
</form>
{% endif %}
</div>
</div>
</div>
<div class="col-md-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-5">Название:</dt>
<dd class="col-sm-7">{{ object.name }}</dd>
<dt class="col-sm-5">Код:</dt>
<dd class="col-sm-7"><code>{{ object.code }}</code></dd>
<dt class="col-sm-5">Тип:</dt>
<dd class="col-sm-7">
{% if object.is_system %}
<span class="badge bg-info">Системный</span>
{% else %}
<span class="badge bg-success">Пользовательский</span>
{% endif %}
</dd>
<dt class="col-sm-5">Статус:</dt>
<dd class="col-sm-7">
{% if object.is_positive_end %}
<span class="badge bg-success">✓ Успешный</span>
{% elif object.is_negative_end %}
<span class="badge bg-danger">✗ Отрицательный</span>
{% else %}
<span class="badge bg-secondary">Промежуточный</span>
{% endif %}
</dd>
<dt class="col-sm-5">Заказов:</dt>
<dd class="col-sm-7"><span class="badge bg-light text-dark">{{ orders_count }}</span></dd>
<dt class="col-sm-5">Создано:</dt>
<dd class="col-sm-7"><small>{{ object.created_at|date:"d.m.Y H:i" }}</small></dd>
<dt class="col-sm-5">Изменено:</dt>
<dd class="col-sm-7"><small>{{ object.updated_at|date:"d.m.Y H:i" }}</small></dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,295 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{% if form.instance.pk %}Редактировать{% else %}Создать{% endif %} статус{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-8">
<h1>
{% if form.instance.pk %}
Редактировать статус: {{ form.instance.name }}
{% else %}
Создать новый статус
{% endif %}
</h1>
{% if is_system %}
<div class="alert alert-info mt-2">
<i class="fas fa-info-circle"></i> Это системный статус. Некоторые поля не могут быть изменены.
</div>
{% endif %}
</div>
<div class="col-md-4 text-end">
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Вернуться к статусам
</a>
</div>
</div>
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Ошибка!</h4>
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<form method="post" novalidate>
{% csrf_token %}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">
{{ form.name.label }}
<span class="text-danger">*</span>
</label>
{{ form.name }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{% for error in form.name.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
Например: Выполнен, В процессе, Возврат
</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.code.id_for_label }}" class="form-label">
{{ form.code.label }}
<span class="text-danger">*</span>
</label>
{{ form.code }}
{% if form.code.errors %}
<div class="invalid-feedback d-block">
{% for error in form.code.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
{% if form.code.field.help_text %}
{{ form.code.field.help_text|safe }}
{% else %}
Латинские буквы, цифры и подчеркивания
{% endif %}
</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.label.id_for_label }}" class="form-label">
{{ form.label.label }}
</label>
{{ form.label }}
{% if form.label.errors %}
<div class="invalid-feedback d-block">
{% for error in form.label.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
Для отображения в интерфейсе (опционально)
</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.color.id_for_label }}" class="form-label">
{{ form.color.label }}
</label>
<div class="input-group">
{{ form.color }}
<span class="input-group-text" id="color-preview" style="width: 60px; background-color: #808080;"></span>
</div>
{% if form.color.errors %}
<div class="invalid-feedback d-block">
{% for error in form.color.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
{{ form.description }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{% for error in form.description.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
Описание для пользователей (опционально)
</small>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.is_positive_end }}
<label class="form-check-label" for="{{ form.is_positive_end.id_for_label }}">
{{ form.is_positive_end.label }}
</label>
<small class="d-block text-muted mt-1">
Отметьте, если это успешный финальный статус (Выполнен)
</small>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.is_negative_end }}
<label class="form-check-label" for="{{ form.is_negative_end.id_for_label }}">
{{ form.is_negative_end.label }}
</label>
<small class="d-block text-muted mt-1">
Отметьте, если это отрицательный финальный статус (Отменен)
</small>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i>
{% if form.instance.pk %}
Сохранить изменения
{% else %}
Создать статус
{% endif %}
</button>
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-times"></i> Отменить
</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header bg-light">
<h6 class="mb-0">Предпросмотр</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Название статуса</label>
<p class="lead">
<span id="preview-name">{{ form.instance.name|default:"Название" }}</span>
</p>
</div>
<div class="mb-3">
<label class="form-label">Код статуса</label>
<code id="preview-code">{{ form.instance.code|default:"код" }}</code>
</div>
<div class="mb-3">
<label class="form-label">Внешний вид</label>
<div style="padding: 10px; border-radius: 4px; background-color: {{ form.instance.color|default:'#808080' }}; color: white; text-align: center;" id="preview-color">
<strong id="preview-color-text">{{ form.instance.name|default:"Статус" }}</strong>
</div>
</div>
<div class="mb-3">
<label class="form-label">Тип статуса</label>
<p id="preview-type">
{% if form.instance.is_system %}
<span class="badge bg-info">Системный</span>
{% else %}
<span class="badge bg-success">Пользовательский</span>
{% endif %}
</p>
</div>
<div class="mb-3">
<label class="form-label">Финальный статус</label>
<p id="preview-end">
{% if form.instance.is_positive_end %}
<span class="badge bg-success">✓ Успешный конец</span>
{% elif form.instance.is_negative_end %}
<span class="badge bg-danger">✗ Отрицательный конец</span>
{% else %}
<span class="badge bg-secondary">Промежуточный</span>
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const nameInput = document.getElementById('{{ form.name.id_for_label }}');
const codeInput = document.getElementById('{{ form.code.id_for_label }}');
const colorInput = document.getElementById('{{ form.color.id_for_label }}');
const positiveEndCheckbox = document.getElementById('{{ form.is_positive_end.id_for_label }}');
const negativeEndCheckbox = document.getElementById('{{ form.is_negative_end.id_for_label }}');
const previewName = document.getElementById('preview-name');
const previewCode = document.getElementById('preview-code');
const previewColor = document.getElementById('preview-color');
const previewColorText = document.getElementById('preview-color-text');
const colorPreview = document.getElementById('color-preview');
const previewEnd = document.getElementById('preview-end');
function updatePreview() {
// Обновляем название
previewName.textContent = nameInput.value || 'Название';
previewColorText.textContent = nameInput.value || 'Статус';
// Обновляем код
previewCode.textContent = codeInput.value || 'код';
// Обновляем цвет
const color = colorInput.value || '#808080';
previewColor.style.backgroundColor = color;
colorPreview.style.backgroundColor = color;
// Обновляем тип конца
if (positiveEndCheckbox.checked) {
previewEnd.innerHTML = '<span class="badge bg-success">✓ Успешный конец</span>';
} else if (negativeEndCheckbox.checked) {
previewEnd.innerHTML = '<span class="badge bg-danger">✗ Отрицательный конец</span>';
} else {
previewEnd.innerHTML = '<span class="badge bg-secondary">Промежуточный</span>';
}
}
nameInput.addEventListener('input', updatePreview);
codeInput.addEventListener('input', updatePreview);
colorInput.addEventListener('change', updatePreview);
positiveEndCheckbox.addEventListener('change', updatePreview);
negativeEndCheckbox.addEventListener('change', updatePreview);
// Инициальное обновление
updatePreview();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Статусы заказов{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-6">
<h1>Статусы заказов</h1>
<p class="text-muted">Управление статусами для заказов вашего магазина</p>
</div>
<div class="col-md-6 text-end">
<a href="{% url 'orders:status_create' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> Создать новый статус
</a>
</div>
</div>
{% if messages %}
<div class="row">
<div class="col-md-12">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 50px;"></th>
<th style="width: 200px;">Название</th>
<th style="width: 150px;">Код</th>
<th style="width: 150px;">Тип</th>
<th style="width: 100px;">Конец</th>
<th style="width: 80px;">Цвет</th>
<th style="width: 80px;">Заказов</th>
<th style="width: 150px;">Действия</th>
</tr>
</thead>
<tbody>
{% for status in statuses %}
<tr>
<td>
<span class="badge bg-secondary">{{ status.order }}</span>
</td>
<td>
<strong>{{ status.name }}</strong>
{% if status.description %}
<br>
<small class="text-muted">{{ status.description|truncatewords:10 }}</small>
{% endif %}
</td>
<td>
<code>{{ status.code }}</code>
</td>
<td>
{% if status.is_system %}
<span class="badge bg-info">Системный</span>
{% else %}
<span class="badge bg-success">Пользовательский</span>
{% endif %}
</td>
<td>
{% if status.is_positive_end %}
<span class="badge bg-success">✓ Успешный</span>
{% elif status.is_negative_end %}
<span class="badge bg-danger">✗ Отрицательный</span>
{% else %}
<span class="badge bg-secondary"></span>
{% endif %}
</td>
<td>
<div style="width: 40px; height: 30px; background-color: {{ status.color }}; border-radius: 4px; border: 1px solid #ccc;" title="{{ status.color }}"></div>
</td>
<td>
<span class="badge bg-light text-dark">{{ status.orders_count }}</span>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'orders:status_edit' status.pk %}" class="btn btn-outline-primary" title="Редактировать">
<i class="fas fa-edit"></i>
</a>
{% if not status.is_system and status.orders_count == 0 %}
<a href="{% url 'orders:status_delete' status.pk %}" class="btn btn-outline-danger" title="Удалить">
<i class="fas fa-trash"></i>
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled title="{% if status.is_system %}Системный статус{% else %}В статусе есть заказы{% endif %}">
<i class="fas fa-trash"></i>
</button>
{% endif %}
{% if not forloop.first %}
<form method="post" action="{% url 'orders:status_move' status.pk %}" class="d-inline" style="display: none;" id="move-up-form-{{ status.pk }}">
{% csrf_token %}
<input type="hidden" name="direction" value="up">
</form>
<button class="btn btn-outline-secondary" onclick="document.getElementById('move-up-form-{{ status.pk }}').submit();" title="Вверх">
<i class="fas fa-arrow-up"></i>
</button>
{% endif %}
{% if not forloop.last %}
<form method="post" action="{% url 'orders:status_move' status.pk %}" class="d-inline" style="display: none;" id="move-down-form-{{ status.pk }}">
{% csrf_token %}
<input type="hidden" name="direction" value="down">
</form>
<button class="btn btn-outline-secondary" onclick="document.getElementById('move-down-form-{{ status.pk }}').submit();" title="Вниз">
<i class="fas fa-arrow-down"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center text-muted py-4">
<i class="fas fa-inbox"></i> Статусы не найдены
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</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 %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% 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 %}
</div>
</div>
</div>
{% endblock %}