From 8fcecb558d0f180b03ca210dba3b68373bced6d7 Mon Sep 17 00:00:00 2001 From: cursor-agent Date: Thu, 26 Feb 2026 14:52:08 +0000 Subject: [PATCH] =?UTF-8?q?Feature:=20=D1=8D=D0=BA=D0=BE=D0=BD=D0=BE=D0=BC?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=B8=20=D1=81=20=D0=BF=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D1=89=D0=B8=D0=BA=D0=BE=D0=BC=20=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D1=85=D0=BE=D0=B4=D0=B0=D0=BC=D0=B8,=20=D0=BE?= =?UTF-8?q?=D1=82=D1=87=D1=91=D1=82=20=D0=BF=D0=BE=20=D0=BC=D0=B0=D1=80?= =?UTF-8?q?=D0=B6=D0=B5=20=D0=B8=20=D1=80=D0=B5=D0=BD=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- HISTORY.md | 15 ++ app/documents/forms.py | 4 +- .../0002_add_customer_order_links.py | 24 +++ app/documents/models.py | 22 +++ app/documents/urls.py | 1 + app/documents/views.py | 11 ++ .../documents/customer_order_detail.html | 157 ++++++++++++++++++ .../documents/customer_order_list.html | 2 +- .../documents/supplier_order_form.html | 7 + 9 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 app/documents/migrations/0002_add_customer_order_links.py create mode 100644 app/templates/documents/customer_order_detail.html diff --git a/HISTORY.md b/HISTORY.md index f098d82..2aafa6f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,20 @@ # История изменений ERP WaterSurf +## 2025-02-26 00:15 UTC – Экономика заказа клиента: связи и отчёт + +**Задача**: Цепочка «Заказ клиента → Поступления денежных средств (несколько) → Заказ поставщику по заказу → Расход на оплату поставщику; плюс прочие расходы по заказу (логистика)». По заказу клиента видеть: сколько поступило, сколько потрачено, маржа, маржинальность, рентабельность. + +**Решение**: +- В модель **SupplierOrder** добавлено поле **customer_order** (FK на CustomerOrder, null/blank) — заказ поставщику оформляется по заказу клиента. +- В модель **CashExpense** добавлено поле **customer_order** (FK на CustomerOrder, null/blank) — расходы по заказу клиента без привязки к заказу поставщику (логистика и т.п.). +- В формах заказа поставщику и расхода денежных средств добавлены поля выбора заказа клиента; в шаблоне заказа поставщику выведено поле «Заказ клиента». +- В модель **CustomerOrder** добавлен метод **get_economics()**: сумма поступлений (cash_inflows), расходы через заказы поставщику (CashExpense по supplier_order.customer_order), прямые расходы по заказу (cash_expenses), итого расходов, маржа, маржинальность % (маржа/поступления), рентабельность % (маржа/расходы). +- Добавлена страница **Просмотр заказа клиента** (CustomerOrderDetail): данные заказа, блок «Экономика заказа» (таблица показателей), списки поступлений, заказов поставщику и прочих расходов по заказу с ссылками на создание/редактирование. В списке заказов клиента номер ведёт на эту страницу. + +**Изменения**: documents/models.py (customer_order в SupplierOrder и CashExpense, get_economics), миграция 0002_add_customer_order_links, documents/forms.py (поля customer_order), supplier_order_form.html (поле заказ клиента), documents/views.py (CustomerOrderDetail), documents/urls.py, шаблон customer_order_detail.html, customer_order_list.html (ссылка с номера на просмотр). + +--- + ## 2025-02-25 23:55 UTC – Дата при открытии формы, без лишней пустой строки при редактировании **Проблема**: При открытии формы редактирования заказа поле «Дата» не подставлялось из БД; в табличной части вместе с существующими строками отображалась лишняя пустая строка. diff --git a/app/documents/forms.py b/app/documents/forms.py index 3c59d68..b77a68b 100644 --- a/app/documents/forms.py +++ b/app/documents/forms.py @@ -82,7 +82,7 @@ class CustomerOrderForm(forms.ModelForm): class SupplierOrderForm(forms.ModelForm): class Meta: model = SupplierOrder - fields = ("date", "number", "organization", "supplier", "currency", "rate", "author") + fields = ("date", "number", "organization", "supplier", "customer_order", "currency", "rate", "author") widgets = { "date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), "number": forms.TextInput(attrs={"size": 15, "maxlength": 15}), @@ -112,7 +112,7 @@ class CashTransferForm(forms.ModelForm): class CashExpenseForm(forms.ModelForm): class Meta: model = CashExpense - fields = ("date", "number", "sender", "amount", "supplier_order", "comment", "author") + fields = ("date", "number", "sender", "amount", "supplier_order", "customer_order", "comment", "author") widgets = { "date": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"), "number": forms.TextInput(attrs={"size": 15, "maxlength": 15}), diff --git a/app/documents/migrations/0002_add_customer_order_links.py b/app/documents/migrations/0002_add_customer_order_links.py new file mode 100644 index 0000000..5e15fe3 --- /dev/null +++ b/app/documents/migrations/0002_add_customer_order_links.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='supplierorder', + name='customer_order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supplier_orders', to='documents.customerorder', verbose_name='Заказ клиента'), + ), + migrations.AddField( + model_name='cashexpense', + name='customer_order', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cash_expenses', to='documents.customerorder', verbose_name='Заказ клиента'), + ), + ] diff --git a/app/documents/models.py b/app/documents/models.py index d02be9f..f18333d 100644 --- a/app/documents/models.py +++ b/app/documents/models.py @@ -2,6 +2,7 @@ """Документы ERP WaterSurf.""" from decimal import Decimal from django.db import models +from django.db.models import Sum from references.models import Currency, OrderKind, Client, Organization, Supplier, Employee, CashAccount, Product class CustomerOrder(models.Model): @@ -18,6 +19,25 @@ class CustomerOrder(models.Model): def __str__(self): return f"{self.number} от {self.date}" def recalc_total(self): self.total_amount = sum((i.price * i.quantity for i in self.items.all()), Decimal("0")) + def get_economics(self): + """Сводка по поступлениям и расходам по заказу: поступления, расходы (через заказы поставщику + прямые), маржа, маржинальность, рентабельность.""" + total_inflows = self.cash_inflows.aggregate(s=Sum("amount"))["s"] or Decimal("0") + expenses_via_supplier = CashExpense.objects.filter(supplier_order__customer_order=self).aggregate(s=Sum("amount"))["s"] or Decimal("0") + expenses_direct = self.cash_expenses.aggregate(s=Sum("amount"))["s"] or Decimal("0") + total_expenses = expenses_via_supplier + expenses_direct + margin = total_inflows - total_expenses + margin_pct = (margin / total_inflows * 100) if total_inflows else None + profitability_pct = (margin / total_expenses * 100) if total_expenses else None + return { + "total_inflows": total_inflows, + "expenses_via_supplier": expenses_via_supplier, + "expenses_direct": expenses_direct, + "total_expenses": total_expenses, + "margin": margin, + "margin_pct": margin_pct, + "profitability_pct": profitability_pct, + } + class CustomerOrderItem(models.Model): document = models.ForeignKey(CustomerOrder, on_delete=models.CASCADE, related_name="items", verbose_name="Заказ") product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар") @@ -38,6 +58,7 @@ class SupplierOrder(models.Model): number = models.CharField("Номер", max_length=50) organization = models.ForeignKey(Organization, on_delete=models.PROTECT, verbose_name="Организация") supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT, verbose_name="Поставщик") + customer_order = models.ForeignKey(CustomerOrder, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Заказ клиента", related_name="supplier_orders") currency = models.ForeignKey(Currency, on_delete=models.PROTECT, verbose_name="Валюта") rate = models.DecimalField("Курс валюты", max_digits=18, decimal_places=6, default=Decimal("1")) total_in_currency = models.DecimalField("Стоимость в валюте", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False) @@ -97,6 +118,7 @@ class CashExpense(models.Model): sender = models.ForeignKey(CashAccount, on_delete=models.PROTECT, verbose_name="Отправитель", related_name="expenses") amount = models.DecimalField("Сумма", max_digits=18, decimal_places=2, default=Decimal("0")) supplier_order = models.ForeignKey(SupplierOrder, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Заказ поставщику", related_name="cash_expenses") + customer_order = models.ForeignKey(CustomerOrder, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Заказ клиента", related_name="cash_expenses") comment = models.CharField("Комментарий", max_length=500, blank=True) author = models.ForeignKey(Employee, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Автор", related_name="cash_expenses") class Meta: diff --git a/app/documents/urls.py b/app/documents/urls.py index e5a7820..ee94f0d 100644 --- a/app/documents/urls.py +++ b/app/documents/urls.py @@ -8,6 +8,7 @@ urlpatterns = [ path("customer-orders/create/", views.CustomerOrderCreate.as_view(), name="customer_order_create"), path("customer-orders//edit/", views.CustomerOrderUpdate.as_view(), name="customer_order_edit"), path("customer-orders//delete/", views.CustomerOrderDelete.as_view(), name="customer_order_delete"), + path("customer-orders//", views.CustomerOrderDetail.as_view(), name="customer_order_detail"), path("supplier-orders/", views.SupplierOrderList.as_view(), name="supplier_order_list"), path("supplier-orders/create/", views.SupplierOrderCreate.as_view(), name="supplier_order_create"), path("supplier-orders//edit/", views.SupplierOrderUpdate.as_view(), name="supplier_order_edit"), diff --git a/app/documents/views.py b/app/documents/views.py index 94cf025..b2d1317 100644 --- a/app/documents/views.py +++ b/app/documents/views.py @@ -107,6 +107,17 @@ class CustomerOrderDelete(LoginRequiredMixin, DeleteView): context_object_name = "object" +class CustomerOrderDetail(LoginRequiredMixin, DetailView): + model = CustomerOrder + template_name = "documents/customer_order_detail.html" + context_object_name = "object" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + ctx["economics"] = self.object.get_economics() + return ctx + + # --- Заказы поставщику --- class SupplierOrderList(LoginRequiredMixin, ListView): model = SupplierOrder diff --git a/app/templates/documents/customer_order_detail.html b/app/templates/documents/customer_order_detail.html new file mode 100644 index 0000000..10ba4b2 --- /dev/null +++ b/app/templates/documents/customer_order_detail.html @@ -0,0 +1,157 @@ +{% extends "base.html" %} +{% block title %}Заказ {{ object.number }} — ERP WaterSurf{% endblock %} +{% block content %} +
+
+

Заказ клиента {{ object.number }} от {{ object.date }}

+ +
+ +
+

Данные заказа

+

Организация: {{ object.organization }}

+

Клиент: {{ object.client }}

+

Вид заказа: {{ object.order_kind }}

+

Стоимость заказа: {{ object.total_amount|floatformat:2 }}

+
+ +
+

Экономика заказа

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Поступило от клиента (всего поступлений по заказу){{ economics.total_inflows|floatformat:2 }}
Расходы: оплата поставщикам (по заказам поставщику по этому заказу)−{{ economics.expenses_via_supplier|floatformat:2 }}
Расходы: прочие по заказу (логистика и т.п.)−{{ economics.expenses_direct|floatformat:2 }}
Всего расходов−{{ economics.total_expenses|floatformat:2 }}
Маржа (поступления − расходы){{ economics.margin|floatformat:2 }}
Маржинальность (маржа / поступления){% if economics.margin_pct is not None %}{{ economics.margin_pct|floatformat:1 }}%{% else %}—{% endif %}
Рентабельность (маржа / расходы){% if economics.profitability_pct is not None %}{{ economics.profitability_pct|floatformat:1 }}%{% else %}—{% endif %}
+
+ +
+

Поступления по заказу

+ {% if object.cash_inflows.all %} +
+ + + + + + + + + + + + {% for inv in object.cash_inflows.all %} + + + + + + + + {% endfor %} + +
ДатаНомерПолучательСумма
{{ inv.date }}{{ inv.number }}{{ inv.recipient }}{{ inv.amount|floatformat:2 }}Изменить
+
+ {% else %} +

Нет поступлений, привязанных к этому заказу.

+ {% endif %} +

+ Оформить поступление

+
+ +
+

Заказы поставщику по этому заказу

+ {% if object.supplier_orders.all %} +
+ + + + + + + + + + + + {% for so in object.supplier_orders.all %} + + + + + + + + {% endfor %} + +
ДатаНомерПоставщикСтоимость
{{ so.date }}{{ so.number }}{{ so.supplier }}{{ so.total_amount|floatformat:2 }}Изменить
+
+ {% else %} +

Нет заказов поставщику по этому заказу.

+ {% endif %} +

+ Создать заказ поставщику

+
+ +
+

Прочие расходы по заказу (логистика и т.п.)

+ {% if object.cash_expenses.all %} +
+ + + + + + + + + + + + {% for ex in object.cash_expenses.all %} + + + + + + + + {% endfor %} + +
ДатаНомерОтправительСумма
{{ ex.date }}{{ ex.number }}{{ ex.sender }}{{ ex.amount|floatformat:2 }}Изменить
+
+ {% else %} +

Нет прочих расходов, привязанных к этому заказу.

+ {% endif %} +

+ Оформить расход

+
+
+{% endblock %} diff --git a/app/templates/documents/customer_order_list.html b/app/templates/documents/customer_order_list.html index 311779c..b922996 100644 --- a/app/templates/documents/customer_order_list.html +++ b/app/templates/documents/customer_order_list.html @@ -23,7 +23,7 @@ {% for obj in object_list %} {{ obj.date }} - {{ obj.number }} + {{ obj.number }} {{ obj.order_kind }} {{ obj.organization }} {{ obj.client }} diff --git a/app/templates/documents/supplier_order_form.html b/app/templates/documents/supplier_order_form.html index c0bdec1..2f431f1 100644 --- a/app/templates/documents/supplier_order_form.html +++ b/app/templates/documents/supplier_order_form.html @@ -29,6 +29,13 @@ {% if form.supplier.errors %}{{ form.supplier.errors.0 }}{% endif %} +
+
+ + {{ form.customer_order }} + {% if form.customer_order.errors %}{{ form.customer_order.errors.0 }}{% endif %} +
+