Fix: количество — только целое 0–99 (модели, формы, JS)
Made-with: Cursor
This commit is contained in:
10
HISTORY.md
10
HISTORY.md
@@ -1,5 +1,15 @@
|
|||||||
# История изменений ERP WaterSurf
|
# История изменений 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 – Форма заказа клиента: сетка, компактные поля, ширина
|
## 2025-02-26 00:50 UTC – Форма заказа клиента: сетка, компактные поля, ширина
|
||||||
|
|
||||||
**Проблема**: На форме заказа клиента поле «Номер» визуально смещено вправо; высота полей и отступы между строками слишком большие; поля «Вид заказа», «Организация», «Клиент», «Автор» узкие.
|
**Проблема**: На форме заказа клиента поле «Номер» визуально смещено вправо; высота полей и отступы между строками слишком большие; поля «Вид заказа», «Организация», «Клиент», «Автор» узкие.
|
||||||
|
|||||||
@@ -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"}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
25
app/documents/migrations/0003_quantity_integer_0_99.py
Normal file
25
app/documents/migrations/0003_quantity_integer_0_99.py
Normal 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='Количество'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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 = "Строка заказа поставщику"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user