Feature: справочник Статусы заказов, поле Статус в заказе клиента, оформление в списке (зелёный/песочный)

Made-with: Cursor
This commit is contained in:
2026-02-26 16:44:39 +00:00
parent 9d1bfadb96
commit 29cf44e278
15 changed files with 151 additions and 4 deletions

View File

@@ -1,5 +1,15 @@
# История изменений ERP WaterSurf # История изменений ERP WaterSurf
## 2025-02-26 17:00 UTC Справочник «Статусы заказов» и поле «Статус заказа» в заказе клиента
**Задача**: Справочник статусов заказов, поле в заказе клиента, отображение в списке с цветовой подложкой («Выполнено» — зелёная, «В работе» — песочная).
**Решение**: В справочниках добавлена модель **OrderStatus** (название). Начальные данные: «В работе», «Выполнено». В модель **CustomerOrder** добавлено поле **status** (FK на OrderStatus, null/blank). В форме заказа клиента добавлено поле выбора статуса (по умолчанию при создании — «В работе»). В списке заказов клиентов добавлена колонка «Статус заказа» после «Номер»; значение выводится бейджем с классом `ws-status-done` (зелёная подложка) для «Выполнено» и `ws-status-inprogress` (песочная подложка) для «В работе».
**Изменения**: references/models.py (OrderStatus), references/admin.py, references/views.py, references/urls.py, base.html (меню «Статусы заказов»), references/migrations 0002, 0003 (данные); documents/models.py (status у CustomerOrder), documents/migrations 0004; documents/forms.py, views.py (форма, get_initial); order_form.html (поле статус); customer_order_list.html (колонка и бейдж); theme.css (ws-status-badge, ws-status-done, ws-status-inprogress).
---
## 2025-02-26 16:50 UTC Общая сумма над блоком Товары; клик по строке списка открывает документ ## 2025-02-26 16:50 UTC Общая сумма над блоком Товары; клик по строке списка открывает документ
**Изменения**: Блок «Общая сумма заказа» перенесён над всей табличной частью «Товары» — сразу под полями Клиент/Автор. В списках заказов клиентов и заказов поставщику документ открывается при клике по строке (в обоих случаях — форма редактирования); клик по ссылкам «Изменить»/«Удалить» не переходит по строке. Добавлены класс строки `ws-row-clickable`, атрибут `data-href` и скрипт перехода. **Изменения**: Блок «Общая сумма заказа» перенесён над всей табличной частью «Товары» — сразу под полями Клиент/Автор. В списках заказов клиентов и заказов поставщику документ открывается при клике по строке (в обоих случаях — форма редактирования); клик по ссылкам «Изменить»/«Удалить» не переходит по строке. Добавлены класс строки `ws-row-clickable`, атрибут `data-href` и скрипт перехода.

View File

@@ -82,7 +82,7 @@ SupplierOrderItemFormSetUpdate = inlineformset_factory(
class CustomerOrderForm(forms.ModelForm): class CustomerOrderForm(forms.ModelForm):
class Meta: class Meta:
model = CustomerOrder model = CustomerOrder
fields = ("date", "number", "order_kind", "organization", "client") fields = ("date", "number", "order_kind", "organization", "client", "status")
widgets = { widgets = {
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), "date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
"number": forms.TextInput(attrs={"size": 15, "maxlength": 15}), "number": forms.TextInput(attrs={"size": 15, "maxlength": 15}),

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.11 on 2026-02-26 16:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documents', '0003_quantity_integer_0_99'),
('references', '0002_order_status'),
]
operations = [
migrations.AddField(
model_name='customerorder',
name='status',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='customer_orders', to='references.orderstatus', verbose_name='Статус заказа'),
),
]

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from references.models import Currency, OrderKind, Client, Organization, Supplier, Employee, CashAccount, Product from references.models import Currency, OrderKind, Client, Organization, Supplier, Employee, CashAccount, Product, OrderStatus
class CustomerOrder(models.Model): class CustomerOrder(models.Model):
date = models.DateField("Дата") date = models.DateField("Дата")
@@ -12,6 +12,14 @@ class CustomerOrder(models.Model):
order_kind = models.ForeignKey(OrderKind, on_delete=models.PROTECT, verbose_name="Вид заказа") order_kind = models.ForeignKey(OrderKind, on_delete=models.PROTECT, verbose_name="Вид заказа")
organization = models.ForeignKey(Organization, on_delete=models.PROTECT, verbose_name="Организация") organization = models.ForeignKey(Organization, on_delete=models.PROTECT, verbose_name="Организация")
client = models.ForeignKey(Client, on_delete=models.PROTECT, verbose_name="Клиент") client = models.ForeignKey(Client, on_delete=models.PROTECT, verbose_name="Клиент")
status = models.ForeignKey(
OrderStatus,
on_delete=models.PROTECT,
verbose_name="Статус заказа",
null=True,
blank=True,
related_name="customer_orders",
)
total_amount = models.DecimalField("Стоимость заказа", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False) total_amount = models.DecimalField("Стоимость заказа", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="customer_orders") author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="customer_orders")
class Meta: class Meta:

View File

@@ -8,7 +8,7 @@ from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from users.utils import get_author_employee from users.utils import get_author_employee
from references.models import Employee from references.models import Employee, OrderStatus
from .models import ( from .models import (
CustomerOrder, CustomerOrder,
SupplierOrder, SupplierOrder,
@@ -67,6 +67,14 @@ class CustomerOrderCreate(LoginRequiredMixin, CreateView):
template_name = "documents/order_form.html" template_name = "documents/order_form.html"
success_url = reverse_lazy("documents:customer_order_list") success_url = reverse_lazy("documents:customer_order_list")
def get_initial(self):
initial = super().get_initial()
if not initial.get("status"):
in_progress = OrderStatus.objects.filter(name="В работе").first()
if in_progress:
initial["status"] = in_progress
return initial
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx["formset"] = CustomerOrderItemFormSet(instance=self.object) if self.object and self.object.pk else CustomerOrderItemFormSet() ctx["formset"] = CustomerOrderItemFormSet(instance=self.object) if self.object and self.object.pk else CustomerOrderItemFormSet()

View File

@@ -8,6 +8,7 @@ from .models import (
Employee, Employee,
CashAccount, CashAccount,
Product, Product,
OrderStatus,
) )
admin.site.register(Currency) admin.site.register(Currency)
@@ -18,3 +19,4 @@ admin.site.register(Supplier)
admin.site.register(Employee) admin.site.register(Employee)
admin.site.register(CashAccount) admin.site.register(CashAccount)
admin.site.register(Product) admin.site.register(Product)
admin.site.register(OrderStatus)

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.2.11 on 2026-02-26 16:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('references', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='OrderStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название')),
],
options={
'verbose_name': 'Статус заказа',
'verbose_name_plural': 'Статусы заказов',
},
),
]

View File

@@ -0,0 +1,24 @@
# Data migration: начальные статусы заказов
from django.db import migrations
def create_default_statuses(apps, schema_editor):
OrderStatus = apps.get_model("references", "OrderStatus")
for name in ["В работе", "Выполнено"]:
OrderStatus.objects.get_or_create(name=name)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("references", "0002_order_status"),
]
operations = [
migrations.RunPython(create_default_statuses, noop),
]

View File

@@ -99,3 +99,15 @@ class Product(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class OrderStatus(models.Model):
"""Статусы заказов."""
name = models.CharField("Название", max_length=100)
class Meta:
verbose_name = "Статус заказа"
verbose_name_plural = "Статусы заказов"
def __str__(self):
return self.name

View File

@@ -36,4 +36,8 @@ urlpatterns = [
path("products/create/", views.ProductCreate.as_view(), name="product_create"), path("products/create/", views.ProductCreate.as_view(), name="product_create"),
path("products/<int:pk>/edit/", views.ProductUpdate.as_view(), name="product_update"), path("products/<int:pk>/edit/", views.ProductUpdate.as_view(), name="product_update"),
path("products/<int:pk>/delete/", views.ProductDelete.as_view(), name="product_delete"), path("products/<int:pk>/delete/", views.ProductDelete.as_view(), name="product_delete"),
path("order-statuses/", views.OrderStatusList.as_view(), name="orderstatus_list"),
path("order-statuses/create/", views.OrderStatusCreate.as_view(), name="orderstatus_create"),
path("order-statuses/<int:pk>/edit/", views.OrderStatusUpdate.as_view(), name="orderstatus_update"),
path("order-statuses/<int:pk>/delete/", views.OrderStatusDelete.as_view(), name="orderstatus_delete"),
] ]

View File

@@ -17,6 +17,7 @@ from .models import (
Employee, Employee,
CashAccount, CashAccount,
Product, Product,
OrderStatus,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -129,3 +130,10 @@ ProductList, ProductCreate, ProductUpdate, ProductDelete = _ref_view(
"references:product_update", "references:product_delete", "references:product_update", "references:product_delete",
"Товары", "Товар", "Товары", "Товар",
) )
# Статусы заказов
OrderStatusList, OrderStatusCreate, OrderStatusUpdate, OrderStatusDelete = _ref_view(
OrderStatus, "references:orderstatus_list", "references:orderstatus_create",
"references:orderstatus_update", "references:orderstatus_delete",
"Статусы заказов", "Статус заказа",
)

View File

@@ -260,6 +260,23 @@ body {
color: var(--ws-danger-dark); color: var(--ws-danger-dark);
} }
/* Бейдж статуса заказа в списке */
.ws-status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
font-size: 0.875em;
font-weight: 500;
}
.ws-status-done {
background: rgba(76, 175, 80, 0.35);
color: #a5d6a7;
}
.ws-status-inprogress {
background: rgba(255, 193, 7, 0.35);
color: #ffe082;
}
.ws-table .ws-num { .ws-table .ws-num {
text-align: right; text-align: right;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;

View File

@@ -34,6 +34,7 @@
<li><a class="dropdown-item" href="{% url 'references:employee_list' %}">Сотрудники</a></li> <li><a class="dropdown-item" href="{% url 'references:employee_list' %}">Сотрудники</a></li>
<li><a class="dropdown-item" href="{% url 'references:cashaccount_list' %}">Счета денежных средств</a></li> <li><a class="dropdown-item" href="{% url 'references:cashaccount_list' %}">Счета денежных средств</a></li>
<li><a class="dropdown-item" href="{% url 'references:product_list' %}">Товары</a></li> <li><a class="dropdown-item" href="{% url 'references:product_list' %}">Товары</a></li>
<li><a class="dropdown-item" href="{% url 'references:orderstatus_list' %}">Статусы заказов</a></li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">

View File

@@ -13,6 +13,7 @@
<tr> <tr>
<th>Дата</th> <th>Дата</th>
<th>Номер</th> <th>Номер</th>
<th>Статус заказа</th>
<th>Вид заказа</th> <th>Вид заказа</th>
<th>Организация</th> <th>Организация</th>
<th>Клиент</th> <th>Клиент</th>
@@ -25,6 +26,7 @@
<tr class="ws-row-clickable" data-href="{% url 'documents:customer_order_edit' obj.pk %}"> <tr class="ws-row-clickable" data-href="{% url 'documents:customer_order_edit' obj.pk %}">
<td>{{ obj.date }}</td> <td>{{ obj.date }}</td>
<td><a href="{% url 'documents:customer_order_edit' obj.pk %}" class="ws-link">{{ obj.number }}</a></td> <td><a href="{% url 'documents:customer_order_edit' obj.pk %}" class="ws-link">{{ obj.number }}</a></td>
<td>{% if obj.status %}<span class="ws-status-badge {% if obj.status.name == 'Выполнено' %}ws-status-done{% elif obj.status.name == 'В работе' %}ws-status-inprogress{% endif %}">{{ obj.status.name }}</span>{% else %}—{% endif %}</td>
<td>{{ obj.order_kind }}</td> <td>{{ obj.order_kind }}</td>
<td>{{ obj.organization }}</td> <td>{{ obj.organization }}</td>
<td>{{ obj.client }}</td> <td>{{ obj.client }}</td>
@@ -36,7 +38,7 @@
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="7" class="ws-empty">Нет заказов.</td></tr> <tr><td colspan="8" class="ws-empty">Нет заказов.</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -35,6 +35,13 @@
{{ form.client }} {{ form.client }}
{% if form.client.errors %}<small class="ws-text-danger">{{ form.client.errors.0 }}</small>{% endif %} {% if form.client.errors %}<small class="ws-text-danger">{{ form.client.errors.0 }}</small>{% endif %}
</div> </div>
<div class="ws-form-group">
<label for="{{ form.status.id_for_label }}">{{ form.status.label }}</label>
{{ form.status }}
{% if form.status.errors %}<small class="ws-text-danger">{{ form.status.errors.0 }}</small>{% endif %}
</div>
</div>
<div class="ws-form-row ws-form-row-2">
<div class="ws-form-group"> <div class="ws-form-group">
<label>Автор</label> <label>Автор</label>
<span class="ws-readonly">{{ author_display|default:"—" }}</span> <span class="ws-readonly">{{ author_display|default:"—" }}</span>