feat: добавление позиций при создании заявки и исправление списка отделов

- Добавлен список departments в контекст request_list view
- Реализовано динамическое добавление позиций (items) прямо в модале создания заявки
- Кнопка 'Добавить позицию' для добавления товаров/услуг
- Каждая позиция содержит: название, описание, количество, ед.изм., цену
- Позиции можно удалять кнопкой X
- JavaScript собирает все позиции и отправляет в API вместе с заявкой
- Поддержка как создания с позициями, так и без них
- Реализовано на обеих страницах: dashboard и request_list
- Все 72 теста проходят успешно
This commit is contained in:
admin i 2025-12-09 13:21:14 +03:00
parent 41eda0e7ac
commit 272b02377f
3 changed files with 219 additions and 10 deletions

View File

@ -124,6 +124,7 @@ def request_list(request):
'scope': scope,
'filters': filters,
'can_view_all': user.is_staff or user.headed_departments.exists(),
'departments': Department.objects.all().order_by('name'),
}
return render(request, 'procurement/request_list.html', context)

View File

@ -275,11 +275,25 @@
<label class="form-label">Желаемая дата получения</label>
<input type="date" name="desired_date" class="form-control">
</div>
</div>
<div class="alert alert-info mt-3 mb-0">
<i class="bi-info-circle me-2"></i>
После создания вы сможете добавить позиции (товары) в заявку.
<div class="col-12">
<hr class="my-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Позиции (товары/услуги)</label>
<button type="button" class="btn btn-sm btn-outline-primary" id="addItemBtn">
<i class="bi-plus-lg me-1"></i>Добавить позицию
</button>
</div>
<div id="itemsList" class="mb-2">
<!-- Позиции будут добавляться динамически -->
</div>
<div class="text-muted small">
<i class="bi-info-circle me-1"></i>
Можно добавить позиции сейчас или после создания заявки
</div>
</div>
</div>
</div>
@ -297,6 +311,84 @@
{% block extra_js %}
<script>
// === УПРАВЛЕНИЕ ПОЗИЦИЯМИ В ЗАЯВКЕ ===
let itemIndex = 0;
// Функция добавления позиции
function addItem() {
itemIndex++;
const itemHtml = `
<div class="card mb-2 item-card" data-index="${itemIndex}">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="card-title mb-0">Позиция #${itemIndex}</h6>
<button type="button" class="btn btn-sm btn-link text-danger p-0" onclick="removeItem(${itemIndex})">
<i class="bi-x-lg"></i>
</button>
</div>
<div class="row g-2">
<div class="col-12">
<input type="text" class="form-control form-control-sm item-name"
placeholder="Название товара/услуги *" required maxlength="255">
</div>
<div class="col-12">
<textarea class="form-control form-control-sm item-description" rows="2"
placeholder="Описание (опционально)" maxlength="1000"></textarea>
</div>
<div class="col-4">
<input type="number" class="form-control form-control-sm item-quantity"
placeholder="Количество *" min="1" step="1" value="1" required>
</div>
<div class="col-4">
<input type="text" class="form-control form-control-sm item-unit"
placeholder="Ед. изм." value="шт" maxlength="20">
</div>
<div class="col-4">
<div class="input-group input-group-sm">
<input type="number" class="form-control item-price"
placeholder="Цена" min="0" step="0.01">
<span class="input-group-text">₽</span>
</div>
</div>
</div>
</div>
</div>
`;
document.getElementById('itemsList').insertAdjacentHTML('beforeend', itemHtml);
}
// Функция удаления позиции
function removeItem(index) {
document.querySelector(`.item-card[data-index="${index}"]`)?.remove();
}
// Обработчик кнопки добавления позиции
document.getElementById('addItemBtn')?.addEventListener('click', addItem);
// Функция сбора данных позиций
function collectItems() {
const items = [];
document.querySelectorAll('.item-card').forEach(card => {
const name = card.querySelector('.item-name').value.trim();
if (name) {
items.push({
name: name,
description: card.querySelector('.item-description').value.trim() || '',
quantity: parseInt(card.querySelector('.item-quantity').value) || 1,
unit: card.querySelector('.item-unit').value.trim() || 'шт',
estimated_unit_price: parseFloat(card.querySelector('.item-price').value) || 0
});
}
});
return items;
}
// Обработка создания заявки
document.getElementById('procCreateForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
@ -310,6 +402,8 @@
try {
const formData = new FormData(form);
const items = collectItems();
const data = {
title: formData.get('title'),
description: formData.get('description') || '',
@ -319,6 +413,11 @@
desired_date: formData.get('desired_date') || null,
};
// Добавляем позиции, если есть
if (items.length > 0) {
data.items = items;
}
const resp = await fetch('/api/procurement/requests/', {
method: 'POST',
headers: {
@ -335,6 +434,11 @@
const request = await resp.json();
// Очистить форму и список позиций
form.reset();
document.getElementById('itemsList').innerHTML = '';
itemIndex = 0;
// Закрыть модалку
const modal = bootstrap.Modal.getInstance(document.querySelector('#procCreateModal'));
modal?.hide();

View File

@ -206,11 +206,25 @@
<label class="form-label">Желаемая дата получения</label>
<input type="date" name="desired_date" class="form-control">
</div>
</div>
<div class="alert alert-info mt-3 mb-0">
<i class="bi-info-circle me-2"></i>
После создания вы сможете добавить позиции (товары) в заявку.
<div class="col-12">
<hr class="my-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Позиции (товары/услуги)</label>
<button type="button" class="btn btn-sm btn-outline-primary" id="addItemBtnList">
<i class="bi-plus-lg me-1"></i>Добавить позицию
</button>
</div>
<div id="itemsListReq" class="mb-2">
<!-- Позиции будут добавляться динамически -->
</div>
<div class="text-muted small">
<i class="bi-info-circle me-1"></i>
Можно добавить позиции сейчас или после создания заявки
</div>
</div>
</div>
</div>
@ -228,6 +242,84 @@
{% block extra_js %}
<script>
// === УПРАВЛЕНИЕ ПОЗИЦИЯМИ В ЗАЯВКЕ (для request_list) ===
let itemIndexReq = 0;
// Функция добавления позиции
function addItemReq() {
itemIndexReq++;
const itemHtml = `
<div class="card mb-2 item-card-req" data-index="${itemIndexReq}">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="card-title mb-0">Позиция #${itemIndexReq}</h6>
<button type="button" class="btn btn-sm btn-link text-danger p-0" onclick="removeItemReq(${itemIndexReq})">
<i class="bi-x-lg"></i>
</button>
</div>
<div class="row g-2">
<div class="col-12">
<input type="text" class="form-control form-control-sm item-name-req"
placeholder="Название товара/услуги *" required maxlength="255">
</div>
<div class="col-12">
<textarea class="form-control form-control-sm item-description-req" rows="2"
placeholder="Описание (опционально)" maxlength="1000"></textarea>
</div>
<div class="col-4">
<input type="number" class="form-control form-control-sm item-quantity-req"
placeholder="Количество *" min="1" step="1" value="1" required>
</div>
<div class="col-4">
<input type="text" class="form-control form-control-sm item-unit-req"
placeholder="Ед. изм." value="шт" maxlength="20">
</div>
<div class="col-4">
<div class="input-group input-group-sm">
<input type="number" class="form-control item-price-req"
placeholder="Цена" min="0" step="0.01">
<span class="input-group-text">₽</span>
</div>
</div>
</div>
</div>
</div>
`;
document.getElementById('itemsListReq').insertAdjacentHTML('beforeend', itemHtml);
}
// Функция удаления позиции
function removeItemReq(index) {
document.querySelector(`.item-card-req[data-index="${index}"]`)?.remove();
}
// Обработчик кнопки добавления позиции
document.getElementById('addItemBtnList')?.addEventListener('click', addItemReq);
// Функция сбора данных позиций
function collectItemsReq() {
const items = [];
document.querySelectorAll('.item-card-req').forEach(card => {
const name = card.querySelector('.item-name-req').value.trim();
if (name) {
items.push({
name: name,
description: card.querySelector('.item-description-req').value.trim() || '',
quantity: parseInt(card.querySelector('.item-quantity-req').value) || 1,
unit: card.querySelector('.item-unit-req').value.trim() || 'шт',
estimated_unit_price: parseFloat(card.querySelector('.item-price-req').value) || 0
});
}
});
return items;
}
// Обработка создания заявки
document.getElementById('procCreateForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
@ -241,6 +333,8 @@
try {
const formData = new FormData(form);
const items = collectItemsReq();
const data = {
title: formData.get('title'),
description: formData.get('description') || '',
@ -250,6 +344,11 @@
desired_date: formData.get('desired_date') || null,
};
// Добавляем позиции, если есть
if (items.length > 0) {
data.items = items;
}
const resp = await fetch('/api/procurement/requests/', {
method: 'POST',
headers: {
@ -266,6 +365,11 @@
const request = await resp.json();
// Очистить форму и список позиций
form.reset();
document.getElementById('itemsListReq').innerHTML = '';
itemIndexReq = 0;
// Закрыть модалку
const modal = bootstrap.Modal.getInstance(document.querySelector('#procCreateModal'));
modal?.hide();