Fix: количество — только целое 0–99 (модели, формы, JS)

Made-with: Cursor
This commit is contained in:
2026-02-26 15:51:04 +00:00
parent 97d19a4659
commit 824d5512c7
6 changed files with 86 additions and 6 deletions

View File

@@ -1,5 +1,15 @@
# История изменений ERP WaterSurf
## 2025-02-26 15:50 UTC Количество: только целое число 099
**Проблема**: Поле «Количество» допускало дробные значения и числа больше двухзначного; требовалось ограничить целыми числами от 0 до 99 везде.
**Решение**: В моделях **CustomerOrderItem** и **SupplierOrderItem** поле `quantity` переведено на `decimal_places=0` и добавлены валидаторы `MinValueValidator(0)` и `MaxValueValidator(99)`. В формах заказа клиента и заказа поставщику поле «Количество» переопределено на `IntegerField(min_value=0, max_value=99)` с виджетом `step=1`. В шаблонах заказов в JS добавлена функция `parseQty()` (целое 099), расчёт суммы строки использует её для количества; при потере фокуса поле количества приводится к целому 099; при добавлении новой строки количество устанавливается в 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 Форма заказа клиента: сетка, компактные поля, ширина
**Проблема**: На форме заказа клиента поле «Номер» визуально смещено вправо; высота полей и отступы между строками слишком большие; поля «Вид заказа», «Организация», «Клиент», «Автор» узкие.

View File

@@ -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"}),
}

View File

@@ -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='Количество'),
),
]

View File

@@ -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 = "Строка заказа поставщику"

View File

@@ -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);

View File

@@ -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);