feat: добавление позиций при создании заявки и исправление списка отделов
- Добавлен список departments в контекст request_list view - Реализовано динамическое добавление позиций (items) прямо в модале создания заявки - Кнопка 'Добавить позицию' для добавления товаров/услуг - Каждая позиция содержит: название, описание, количество, ед.изм., цену - Позиции можно удалять кнопкой X - JavaScript собирает все позиции и отправляет в API вместе с заявкой - Поддержка как создания с позициями, так и без них - Реализовано на обеих страницах: dashboard и request_list - Все 72 теста проходят успешно
This commit is contained in:
parent
41eda0e7ac
commit
272b02377f
@ -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)
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user