Feature: справочник Статусы заказов, поле Статус в заказе клиента, оформление в списке (зелёный/песочный)
Made-with: Cursor
This commit is contained in:
10
HISTORY.md
10
HISTORY.md
@@ -1,5 +1,15 @@
|
||||
# История изменений 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 – Общая сумма над блоком Товары; клик по строке списка открывает документ
|
||||
|
||||
**Изменения**: Блок «Общая сумма заказа» перенесён над всей табличной частью «Товары» — сразу под полями Клиент/Автор. В списках заказов клиентов и заказов поставщику документ открывается при клике по строке (в обоих случаях — форма редактирования); клик по ссылкам «Изменить»/«Удалить» не переходит по строке. Добавлены класс строки `ws-row-clickable`, атрибут `data-href` и скрипт перехода.
|
||||
|
||||
@@ -82,7 +82,7 @@ SupplierOrderItemFormSetUpdate = inlineformset_factory(
|
||||
class CustomerOrderForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomerOrder
|
||||
fields = ("date", "number", "order_kind", "organization", "client")
|
||||
fields = ("date", "number", "order_kind", "organization", "client", "status")
|
||||
widgets = {
|
||||
"date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
|
||||
"number": forms.TextInput(attrs={"size": 15, "maxlength": 15}),
|
||||
|
||||
20
app/documents/migrations/0004_customer_order_status.py
Normal file
20
app/documents/migrations/0004_customer_order_status.py
Normal 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='Статус заказа'),
|
||||
),
|
||||
]
|
||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
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):
|
||||
date = models.DateField("Дата")
|
||||
@@ -12,6 +12,14 @@ class CustomerOrder(models.Model):
|
||||
order_kind = models.ForeignKey(OrderKind, 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="Клиент")
|
||||
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)
|
||||
author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="customer_orders")
|
||||
class Meta:
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from users.utils import get_author_employee
|
||||
from references.models import Employee
|
||||
from references.models import Employee, OrderStatus
|
||||
from .models import (
|
||||
CustomerOrder,
|
||||
SupplierOrder,
|
||||
@@ -67,6 +67,14 @@ class CustomerOrderCreate(LoginRequiredMixin, CreateView):
|
||||
template_name = "documents/order_form.html"
|
||||
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):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx["formset"] = CustomerOrderItemFormSet(instance=self.object) if self.object and self.object.pk else CustomerOrderItemFormSet()
|
||||
|
||||
@@ -8,6 +8,7 @@ from .models import (
|
||||
Employee,
|
||||
CashAccount,
|
||||
Product,
|
||||
OrderStatus,
|
||||
)
|
||||
|
||||
admin.site.register(Currency)
|
||||
@@ -18,3 +19,4 @@ admin.site.register(Supplier)
|
||||
admin.site.register(Employee)
|
||||
admin.site.register(CashAccount)
|
||||
admin.site.register(Product)
|
||||
admin.site.register(OrderStatus)
|
||||
|
||||
24
app/references/migrations/0002_order_status.py
Normal file
24
app/references/migrations/0002_order_status.py
Normal 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': 'Статусы заказов',
|
||||
},
|
||||
),
|
||||
]
|
||||
24
app/references/migrations/0003_orderstatus_data.py
Normal file
24
app/references/migrations/0003_orderstatus_data.py
Normal 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),
|
||||
]
|
||||
@@ -99,3 +99,15 @@ class Product(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
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
|
||||
|
||||
@@ -36,4 +36,8 @@ urlpatterns = [
|
||||
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>/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"),
|
||||
]
|
||||
|
||||
@@ -17,6 +17,7 @@ from .models import (
|
||||
Employee,
|
||||
CashAccount,
|
||||
Product,
|
||||
OrderStatus,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -129,3 +130,10 @@ ProductList, ProductCreate, ProductUpdate, ProductDelete = _ref_view(
|
||||
"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",
|
||||
"Статусы заказов", "Статус заказа",
|
||||
)
|
||||
|
||||
@@ -260,6 +260,23 @@ body {
|
||||
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 {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<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:product_list' %}">Товары</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'references:orderstatus_list' %}">Статусы заказов</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<tr>
|
||||
<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 %}">
|
||||
<td>{{ obj.date }}</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.organization }}</td>
|
||||
<td>{{ obj.client }}</td>
|
||||
@@ -36,7 +38,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="7" class="ws-empty">Нет заказов.</td></tr>
|
||||
<tr><td colspan="8" class="ws-empty">Нет заказов.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
{{ form.client }}
|
||||
{% if form.client.errors %}<small class="ws-text-danger">{{ form.client.errors.0 }}</small>{% endif %}
|
||||
</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">
|
||||
<label>Автор</label>
|
||||
<span class="ws-readonly">{{ author_display|default:"—" }}</span>
|
||||
|
||||
Reference in New Issue
Block a user