From 824d5512c78d831aaa8b663dc703a1af7bf1f9bc Mon Sep 17 00:00:00 2001 From: cursor-agent Date: Thu, 26 Feb 2026 15:51:04 +0000 Subject: [PATCH] =?UTF-8?q?Fix:=20=D0=BA=D0=BE=D0=BB=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=BE=20=E2=80=94=20=D1=82=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=BE=20=D1=86=D0=B5=D0=BB=D0=BE=D0=B5=200=E2=80=9399=20?= =?UTF-8?q?(=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8,=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D1=8B,=20JS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- HISTORY.md | 10 ++++++++ app/documents/forms.py | 14 +++++++++-- .../migrations/0003_quantity_integer_0_99.py | 25 +++++++++++++++++++ app/documents/models.py | 17 +++++++++++-- app/templates/documents/order_form.html | 13 +++++++++- .../documents/supplier_order_form.html | 13 +++++++++- 6 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 app/documents/migrations/0003_quantity_integer_0_99.py diff --git a/HISTORY.md b/HISTORY.md index 22e413b..02a4f16 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,15 @@ # История изменений ERP WaterSurf +## 2025-02-26 15:50 UTC – Количество: только целое число 0–99 + +**Проблема**: Поле «Количество» допускало дробные значения и числа больше двухзначного; требовалось ограничить целыми числами от 0 до 99 везде. + +**Решение**: В моделях **CustomerOrderItem** и **SupplierOrderItem** поле `quantity` переведено на `decimal_places=0` и добавлены валидаторы `MinValueValidator(0)` и `MaxValueValidator(99)`. В формах заказа клиента и заказа поставщику поле «Количество» переопределено на `IntegerField(min_value=0, max_value=99)` с виджетом `step=1`. В шаблонах заказов в JS добавлена функция `parseQty()` (целое 0–99), расчёт суммы строки использует её для количества; при потере фокуса поле количества приводится к целому 0–99; при добавлении новой строки количество устанавливается в 1. + +**Изменения**: documents/models.py (quantity: decimal_places=0, validators), documents/forms.py (IntegerField для quantity), order_form.html и supplier_order_form.html (parseQty, focusout, клон с quantity=1), миграция 0003_quantity_integer_0_99. + +--- + ## 2025-02-26 00:50 UTC – Форма заказа клиента: сетка, компактные поля, ширина **Проблема**: На форме заказа клиента поле «Номер» визуально смещено вправо; высота полей и отступы между строками слишком большие; поля «Вид заказа», «Организация», «Клиент», «Автор» узкие. diff --git a/app/documents/forms.py b/app/documents/forms.py index b77a68b..18857a8 100644 --- a/app/documents/forms.py +++ b/app/documents/forms.py @@ -13,22 +13,32 @@ from django.forms import inlineformset_factory class CustomerOrderItemForm(forms.ModelForm): + quantity = forms.IntegerField( + min_value=0, + max_value=99, + widget=forms.NumberInput(attrs={"size": 3, "min": 0, "max": 99, "step": 1, "style": "width: 4ch"}), + ) + class Meta: model = CustomerOrderItem fields = ("product", "price", "currency", "quantity") widgets = { "price": forms.TextInput(attrs={"inputmode": "decimal", "class": "ws-price-input"}), - "quantity": forms.NumberInput(attrs={"size": 3, "min": 0, "max": 99, "style": "width: 4ch"}), } class SupplierOrderItemForm(forms.ModelForm): + quantity = forms.IntegerField( + min_value=0, + max_value=99, + widget=forms.NumberInput(attrs={"size": 3, "min": 0, "max": 99, "step": 1, "style": "width: 4ch"}), + ) + class Meta: model = SupplierOrderItem fields = ("product", "price", "currency", "quantity") widgets = { "price": forms.TextInput(attrs={"inputmode": "decimal", "class": "ws-price-input"}), - "quantity": forms.NumberInput(attrs={"size": 3, "min": 0, "max": 99, "style": "width: 4ch"}), } diff --git a/app/documents/migrations/0003_quantity_integer_0_99.py b/app/documents/migrations/0003_quantity_integer_0_99.py new file mode 100644 index 0000000..873c445 --- /dev/null +++ b/app/documents/migrations/0003_quantity_integer_0_99.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.11 on 2026-02-26 15:47 + +import django.core.validators +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0002_add_customer_order_links'), + ] + + operations = [ + migrations.AlterField( + model_name='customerorderitem', + name='quantity', + field=models.DecimalField(decimal_places=0, default=Decimal('1'), max_digits=18, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)], verbose_name='Количество'), + ), + migrations.AlterField( + model_name='supplierorderitem', + name='quantity', + field=models.DecimalField(decimal_places=0, default=Decimal('1'), max_digits=18, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(99)], verbose_name='Количество'), + ), + ] diff --git a/app/documents/models.py b/app/documents/models.py index f18333d..93a98a7 100644 --- a/app/documents/models.py +++ b/app/documents/models.py @@ -3,6 +3,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 class CustomerOrder(models.Model): @@ -43,7 +44,13 @@ class CustomerOrderItem(models.Model): product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар") price = models.DecimalField("Цена", max_digits=18, decimal_places=2, default=Decimal("0")) currency = models.ForeignKey(Currency, on_delete=models.PROTECT, verbose_name="Валюта", null=True, blank=True) - quantity = models.DecimalField("Количество", max_digits=18, decimal_places=4, default=Decimal("1")) + quantity = models.DecimalField( + "Количество", + max_digits=18, + decimal_places=0, + default=Decimal("1"), + validators=[MinValueValidator(0), MaxValueValidator(99)], + ) amount = models.DecimalField("Стоимость", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False) class Meta: verbose_name = "Строка заказа клиента" @@ -77,7 +84,13 @@ class SupplierOrderItem(models.Model): product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар") price = models.DecimalField("Цена", max_digits=18, decimal_places=2, default=Decimal("0")) currency = models.ForeignKey(Currency, on_delete=models.PROTECT, verbose_name="Валюта", null=True, blank=True) - quantity = models.DecimalField("Количество", max_digits=18, decimal_places=4, default=Decimal("1")) + quantity = models.DecimalField( + "Количество", + max_digits=18, + decimal_places=0, + default=Decimal("1"), + validators=[MinValueValidator(0), MaxValueValidator(99)], + ) amount = models.DecimalField("Стоимость", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False) class Meta: verbose_name = "Строка заказа поставщику" diff --git a/app/templates/documents/order_form.html b/app/templates/documents/order_form.html index 30e037f..a297f9e 100644 --- a/app/templates/documents/order_form.html +++ b/app/templates/documents/order_form.html @@ -112,11 +112,16 @@ function parseNum(s) { return parseFloat(String(s).replace(/\s/g, '').replace(',', '.')) || 0; } + function parseQty(s) { + var n = parseInt(String(s).replace(/\s/g, ''), 10); + if (isNaN(n) || n < 0) return 0; + return n > 99 ? 99 : n; + } function updateRowAmounts() { document.querySelectorAll('#order-items .item-row').forEach(function(row) { var priceInput = row.querySelector('input[name$="-price"]'); - var qty = parseNum(row.querySelector('input[name$="-quantity"]')?.value); + var qty = parseQty(row.querySelector('input[name$="-quantity"]')?.value); var price = parseNum(priceInput?.value); var el = row.querySelector('.row-amount'); if (el) el.textContent = formatNum(price * qty); @@ -143,6 +148,10 @@ var n = parseNum(e.target.value); e.target.value = isNaN(n) ? '' : formatNum(n); } + if (e.target.name && e.target.name.indexOf('-quantity') !== -1) { + var q = parseQty(e.target.value); + e.target.value = String(q); + } }); updateRowAmounts(); @@ -199,6 +208,8 @@ el.value = ''; } }); + var qtyInput = clone.querySelector('input[name$="-quantity"]'); + if (qtyInput) qtyInput.value = '1'; var amountCell = clone.querySelector('.row-amount'); if (amountCell) amountCell.textContent = '—'; tbody.appendChild(clone); diff --git a/app/templates/documents/supplier_order_form.html b/app/templates/documents/supplier_order_form.html index 2f431f1..4975220 100644 --- a/app/templates/documents/supplier_order_form.html +++ b/app/templates/documents/supplier_order_form.html @@ -124,11 +124,16 @@ function parseNum(s) { return parseFloat(String(s).replace(/\s/g, '').replace(',', '.')) || 0; } + function parseQty(s) { + var n = parseInt(String(s).replace(/\s/g, ''), 10); + if (isNaN(n) || n < 0) return 0; + return n > 99 ? 99 : n; + } function updateRowAmounts() { document.querySelectorAll('#supplier-order-items .item-row').forEach(function(row) { var priceInput = row.querySelector('input[name$="-price"]'); - var qty = parseNum(row.querySelector('input[name$="-quantity"]')?.value); + var qty = parseQty(row.querySelector('input[name$="-quantity"]')?.value); var price = parseNum(priceInput?.value); var el = row.querySelector('.row-amount'); if (el) el.textContent = formatNum(price * qty); @@ -155,6 +160,10 @@ var n = parseNum(e.target.value); e.target.value = isNaN(n) ? '' : formatNum(n); } + if (e.target.name && e.target.name.indexOf('-quantity') !== -1) { + var q = parseQty(e.target.value); + e.target.value = String(q); + } }); updateRowAmounts(); @@ -209,6 +218,8 @@ el.value = ''; } }); + var qtyInput = clone.querySelector('input[name$="-quantity"]'); + if (qtyInput) qtyInput.value = '1'; var amountCell = clone.querySelector('.row-amount'); if (amountCell) amountCell.textContent = '—'; tbody.appendChild(clone);