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 # История изменений 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 Форма заказа клиента: сетка, компактные поля, ширина ## 2025-02-26 00:50 UTC Форма заказа клиента: сетка, компактные поля, ширина
**Проблема**: На форме заказа клиента поле «Номер» визуально смещено вправо; высота полей и отступы между строками слишком большие; поля «Вид заказа», «Организация», «Клиент», «Автор» узкие. **Проблема**: На форме заказа клиента поле «Номер» визуально смещено вправо; высота полей и отступы между строками слишком большие; поля «Вид заказа», «Организация», «Клиент», «Автор» узкие.

View File

@@ -13,22 +13,32 @@ from django.forms import inlineformset_factory
class CustomerOrderItemForm(forms.ModelForm): 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: class Meta:
model = CustomerOrderItem model = CustomerOrderItem
fields = ("product", "price", "currency", "quantity") fields = ("product", "price", "currency", "quantity")
widgets = { widgets = {
"price": forms.TextInput(attrs={"inputmode": "decimal", "class": "ws-price-input"}), "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): 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: class Meta:
model = SupplierOrderItem model = SupplierOrderItem
fields = ("product", "price", "currency", "quantity") fields = ("product", "price", "currency", "quantity")
widgets = { widgets = {
"price": forms.TextInput(attrs={"inputmode": "decimal", "class": "ws-price-input"}), "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 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 references.models import Currency, OrderKind, Client, Organization, Supplier, Employee, CashAccount, Product from references.models import Currency, OrderKind, Client, Organization, Supplier, Employee, CashAccount, Product
class CustomerOrder(models.Model): class CustomerOrder(models.Model):
@@ -43,7 +44,13 @@ class CustomerOrderItem(models.Model):
product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар") product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар")
price = models.DecimalField("Цена", max_digits=18, decimal_places=2, default=Decimal("0")) 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) 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) amount = models.DecimalField("Стоимость", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
class Meta: class Meta:
verbose_name = "Строка заказа клиента" verbose_name = "Строка заказа клиента"
@@ -77,7 +84,13 @@ class SupplierOrderItem(models.Model):
product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар") product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name="Товар")
price = models.DecimalField("Цена", max_digits=18, decimal_places=2, default=Decimal("0")) 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) 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) amount = models.DecimalField("Стоимость", max_digits=18, decimal_places=2, default=Decimal("0"), editable=False)
class Meta: class Meta:
verbose_name = "Строка заказа поставщику" verbose_name = "Строка заказа поставщику"

View File

@@ -112,11 +112,16 @@
function parseNum(s) { function parseNum(s) {
return parseFloat(String(s).replace(/\s/g, '').replace(',', '.')) || 0; 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() { function updateRowAmounts() {
document.querySelectorAll('#order-items .item-row').forEach(function(row) { document.querySelectorAll('#order-items .item-row').forEach(function(row) {
var priceInput = row.querySelector('input[name$="-price"]'); 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 price = parseNum(priceInput?.value);
var el = row.querySelector('.row-amount'); var el = row.querySelector('.row-amount');
if (el) el.textContent = formatNum(price * qty); if (el) el.textContent = formatNum(price * qty);
@@ -143,6 +148,10 @@
var n = parseNum(e.target.value); var n = parseNum(e.target.value);
e.target.value = isNaN(n) ? '' : formatNum(n); 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(); updateRowAmounts();
@@ -199,6 +208,8 @@
el.value = ''; el.value = '';
} }
}); });
var qtyInput = clone.querySelector('input[name$="-quantity"]');
if (qtyInput) qtyInput.value = '1';
var amountCell = clone.querySelector('.row-amount'); var amountCell = clone.querySelector('.row-amount');
if (amountCell) amountCell.textContent = '—'; if (amountCell) amountCell.textContent = '—';
tbody.appendChild(clone); tbody.appendChild(clone);

View File

@@ -124,11 +124,16 @@
function parseNum(s) { function parseNum(s) {
return parseFloat(String(s).replace(/\s/g, '').replace(',', '.')) || 0; 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() { function updateRowAmounts() {
document.querySelectorAll('#supplier-order-items .item-row').forEach(function(row) { document.querySelectorAll('#supplier-order-items .item-row').forEach(function(row) {
var priceInput = row.querySelector('input[name$="-price"]'); 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 price = parseNum(priceInput?.value);
var el = row.querySelector('.row-amount'); var el = row.querySelector('.row-amount');
if (el) el.textContent = formatNum(price * qty); if (el) el.textContent = formatNum(price * qty);
@@ -155,6 +160,10 @@
var n = parseNum(e.target.value); var n = parseNum(e.target.value);
e.target.value = isNaN(n) ? '' : formatNum(n); 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(); updateRowAmounts();
@@ -209,6 +218,8 @@
el.value = ''; el.value = '';
} }
}); });
var qtyInput = clone.querySelector('input[name$="-quantity"]');
if (qtyInput) qtyInput.value = '1';
var amountCell = clone.querySelector('.row-amount'); var amountCell = clone.querySelector('.row-amount');
if (amountCell) amountCell.textContent = '—'; if (amountCell) amountCell.textContent = '—';
tbody.appendChild(clone); tbody.appendChild(clone);