Теперь сделки на странице планирования
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s

This commit is contained in:
2026-03-31 08:31:54 +03:00
parent c9ff66a36b
commit c2778d9ec8
8 changed files with 453 additions and 82 deletions

View File

@@ -3,10 +3,25 @@
{% block content %}
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<h3 class="text-accent mb-0"><i class="bi bi-kanban me-2"></i>Планирование</h3>
<a class="btn btn-outline-accent btn-sm" href="{% url 'task_add' %}">
<i class="bi bi-plus-lg me-1"></i>Добавить
</a>
<div class="d-flex align-items-center gap-3">
<h3 class="text-accent mb-0"><i class="bi bi-kanban me-2"></i>Планирование</h3>
<form method="get" class="d-flex align-items-center gap-2">
<span class="small text-muted">Сделки:</span>
<div class="d-flex flex-wrap gap-1">
<input type="radio" class="btn-check" name="status" id="deal_s_work" value="work" {% if selected_status == 'work' %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-primary btn-sm" for="deal_s_work">В работе</label>
<input type="radio" class="btn-check" name="status" id="deal_s_lead" value="lead" {% if selected_status == 'lead' %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-secondary btn-sm" for="deal_s_lead">Зашла</label>
<input type="radio" class="btn-check" name="status" id="deal_s_done" value="done" {% if selected_status == 'done' %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-success btn-sm" for="deal_s_done">Завершена</label>
</div>
</form>
</div>
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="create">
<i class="bi bi-plus-lg me-1"></i>Добавить сделку
</button>
</div>
<div class="card-body p-0">
@@ -15,108 +30,232 @@
<thead>
<tr class="table-custom-header">
<th>Сделка</th>
<th>Деталь</th>
<th>Материал</th>
<th>Размер</th>
<th class="text-center">Надо</th>
<th class="text-center">Сделано</th>
<th class="text-center">В плане</th>
<th class="text-center">Осталось</th>
<th class="text-end">Действия</th>
<th>Заказчик</th>
<th>Описание</th>
<th style="width: 140px;">Статус</th>
</tr>
</thead>
<tbody>
{% for t in tasks %}
<tr>
<td><span class="text-accent fw-bold">{{ t.deal.number }}</span></td>
<td class="fw-bold">{{ t.drawing_name|default:"Б/ч" }}</td>
<td class="small text-muted">{{ t.material.full_name|default:t.material.name }}</td>
<td class="small">{{ t.size_value }}</td>
<td class="text-center">{{ t.quantity_ordered }}</td>
<td class="text-center">{{ t.done_qty }}</td>
<td class="text-center">{{ t.planned_qty }}</td>
<td class="text-center">{{ t.remaining_qty }}</td>
<td class="text-end">
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#addToPlanModal"
data-task-id="{{ t.id }}"
data-task-name="{{ t.drawing_name|default:'Б/ч' }}"
data-task-deal="{{ t.deal.number }}"
data-task-rem="{{ t.remaining_qty }}"
>
<i class="bi bi-plus-lg me-1"></i>В план
</button>
{% for d in deals %}
<tr class="planning-row" style="cursor:pointer" data-href="{% url 'planning_deal' d.id %}">
<td><span class="text-accent fw-bold">{{ d.number }}</span></td>
<td class="small">{{ d.company.name|default:"-" }}</td>
<td class="small text-muted">{{ d.description|default:"" }}</td>
<td>
<span class="badge {% if d.status == 'work' %}bg-primary{% elif d.status == 'done' %}bg-success{% else %}bg-secondary{% endif %}">{{ d.get_status_display }}</span>
</td>
</tr>
{% empty %}
<tr><td colspan="9" class="text-center p-5 text-muted">Заданий не найдено</td></tr>
<tr><td colspan="4" class="text-center p-5 text-muted">Сделок не найдено</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="addToPlanModal" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="dealModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'planning_add' %}" class="modal-content border-secondary">
{% csrf_token %}
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Добавить в план</h5>
<h5 class="modal-title">Сделка</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" name="task_id" id="modalTaskId">
<div class="small text-muted mb-2" id="modalTaskTitle"></div>
<input type="hidden" id="dealId">
<div class="mb-3">
<label class="form-label small text-muted">Станок</label>
<select class="form-select border-secondary" name="machine_id" required>
{% for m in machines %}
<option value="{{ m.id }}">{{ m.name }}</option>
{% endfor %}
<label class="form-label small text-muted">№ Сделки</label>
<input type="text" class="form-control border-secondary" id="dealNumber">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Статус</label>
<select class="form-select border-secondary" id="dealStatus">
<option value="lead">Зашла</option>
<option value="work" selected>В работе</option>
<option value="done">Завершена</option>
</select>
</div>
<div class="mb-2">
<label class="form-label small text-muted">Сколько в план (шт)</label>
<input type="number" min="1" class="form-control border-secondary" name="quantity_plan" id="modalQty" required>
<div class="mb-3">
<label class="form-label small text-muted">Компания</label>
<div class="d-flex gap-2">
<select class="form-select border-secondary" id="dealCompany">
<option value="">— не выбрано —</option>
{% for c in companies %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-accent btn-sm" id="openCompanyModalBtn">Создать</button>
</div>
</div>
<div class="mb-0">
<label class="form-label small text-muted">Описание</label>
<textarea class="form-control border-secondary" rows="3" id="dealDescription"></textarea>
</div>
<div class="small text-muted" id="modalHint"></div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-outline-accent">Добавить</button>
<button type="button" class="btn btn-outline-accent" id="dealSaveBtn">Сохранить</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="companyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Компания</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="companyId">
<div class="mb-3">
<label class="form-label small text-muted">Название</label>
<input type="text" class="form-control border-secondary" id="companyName">
</div>
<div class="mb-0">
<label class="form-label small text-muted">Примечание</label>
<textarea class="form-control border-secondary" rows="3" id="companyDescription"></textarea>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="companySaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('addToPlanModal');
if (!modal) return;
modal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
const taskId = btn.getAttribute('data-task-id');
const name = btn.getAttribute('data-task-name');
const deal = btn.getAttribute('data-task-deal');
const rem = btn.getAttribute('data-task-rem');
document.getElementById('modalTaskId').value = taskId;
document.getElementById('modalTaskTitle').textContent = `Сделка ${deal} · ${name}`;
document.getElementById('modalHint').textContent = rem !== null ? `Осталось: ${rem} шт` : '';
const qty = document.getElementById('modalQty');
qty.value = '';
if (rem && !isNaN(parseInt(rem, 10))) qty.max = Math.max(1, parseInt(rem, 10));
else qty.removeAttribute('max');
document.querySelectorAll('tr.planning-row[data-href]').forEach(function (row) {
row.addEventListener('click', function () {
const href = row.getAttribute('data-href');
if (href) window.location.href = href;
});
});
const dealModal = document.getElementById('dealModal');
const dealId = document.getElementById('dealId');
const dealNumber = document.getElementById('dealNumber');
const dealStatus = document.getElementById('dealStatus');
const dealCompany = document.getElementById('dealCompany');
const dealDescription = document.getElementById('dealDescription');
const dealSaveBtn = document.getElementById('dealSaveBtn');
const openCompanyModalBtn = document.getElementById('openCompanyModalBtn');
const companyModal = document.getElementById('companyModal');
const companyId = document.getElementById('companyId');
const companyName = document.getElementById('companyName');
const companyDescription = document.getElementById('companyDescription');
const companySaveBtn = document.getElementById('companySaveBtn');
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
async function postForm(url, data) {
const csrftoken = getCookie('csrftoken');
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'X-CSRFToken': csrftoken,
},
body: new URLSearchParams(data).toString(),
});
if (!res.ok) throw new Error('request_failed');
return await res.json();
}
function upsertSelectOption(select, id, label) {
if (!select) return;
let opt = select.querySelector(`option[value="${id}"]`);
if (!opt) {
opt = document.createElement('option');
opt.value = String(id);
select.appendChild(opt);
}
opt.textContent = label;
select.value = String(id);
}
function openChildModal(parentEl, childEl) {
if (!parentEl || !childEl) return;
childEl.dataset.returnTo = parentEl.id;
const parent = bootstrap.Modal.getOrCreateInstance(parentEl);
const child = bootstrap.Modal.getOrCreateInstance(childEl);
parentEl.addEventListener('hidden.bs.modal', function () {
child.show();
}, { once: true });
parent.hide();
}
function returnToParent(childEl) {
const returnId = childEl ? childEl.dataset.returnTo : '';
if (!returnId) return;
delete childEl.dataset.returnTo;
const parentEl = document.getElementById(returnId);
if (parentEl) bootstrap.Modal.getOrCreateInstance(parentEl).show();
}
if (companyModal) companyModal.addEventListener('hidden.bs.modal', function () { returnToParent(companyModal); });
if (openCompanyModalBtn) {
openCompanyModalBtn.addEventListener('click', function () {
openChildModal(dealModal, companyModal);
});
}
if (dealModal) {
dealModal.addEventListener('show.bs.modal', function (event) {
if (!event.relatedTarget) return;
const mode = event.relatedTarget.getAttribute('data-mode') || 'create';
if (mode !== 'create') return;
dealId.value = '';
dealNumber.value = '';
dealDescription.value = '';
if (dealStatus) dealStatus.value = 'work';
if (dealCompany) dealCompany.value = '';
});
}
if (companyModal) {
companyModal.addEventListener('show.bs.modal', function () {
companyId.value = '';
companyName.value = '';
companyDescription.value = '';
});
}
if (companySaveBtn) {
companySaveBtn.addEventListener('click', async function () {
const payload = {
id: companyId.value,
name: companyName.value,
description: companyDescription.value,
};
const data = await postForm('{% url "company_upsert" %}', payload);
upsertSelectOption(dealCompany, data.id, data.label);
bootstrap.Modal.getInstance(companyModal).hide();
});
}
if (dealSaveBtn) {
dealSaveBtn.addEventListener('click', async function () {
const payload = {
id: dealId.value,
number: dealNumber.value,
status: dealStatus ? dealStatus.value : 'work',
company_id: dealCompany.value,
description: dealDescription.value,
};
await postForm('{% url "deal_upsert" %}', payload);
window.location.reload();
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,136 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<div>
<h3 class="text-accent mb-1">
<i class="bi bi-briefcase me-2"></i>Сделка {{ deal.number }}
</h3>
<div class="small text-muted">
{% if deal.company %}{{ deal.company.name }}{% else %}—{% endif %}
{% if deal.description %} · {{ deal.description }}{% endif %}
</div>
</div>
<div class="d-flex gap-2">
<span class="badge {% if deal.status == 'work' %}bg-primary{% elif deal.status == 'done' %}bg-success{% else %}bg-secondary{% endif %} align-self-center">
{{ deal.get_status_display }}
</span>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
<a class="btn btn-outline-accent btn-sm" href="{% url 'task_add' %}?deal={{ deal.id }}">
<i class="bi bi-plus-lg me-1"></i>Добавить деталь
</a>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th>Деталь</th>
<th>Материал</th>
<th>Размер</th>
<th class="text-center">Надо</th>
<th class="text-center">Сделано</th>
<th class="text-center">В плане</th>
<th class="text-center">Осталось</th>
<th class="text-end">Действия</th>
</tr>
</thead>
<tbody>
{% for t in tasks %}
<tr>
<td class="fw-bold">{{ t.drawing_name|default:"Б/ч" }}</td>
<td class="small text-muted">{{ t.material.full_name|default:t.material.name }}</td>
<td class="small">{{ t.size_value }}</td>
<td class="text-center">{{ t.quantity_ordered }}</td>
<td class="text-center">{{ t.done_qty }}</td>
<td class="text-center">{{ t.planned_qty }}</td>
<td class="text-center">{{ t.remaining_qty }}</td>
<td class="text-end">
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#addToPlanModal"
data-task-id="{{ t.id }}"
data-task-name="{{ t.drawing_name|default:'Б/ч' }}"
data-task-rem="{{ t.remaining_qty }}"
>
<i class="bi bi-plus-lg me-1"></i>В план
</button>
</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center p-5 text-muted">Деталей не найдено</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="addToPlanModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'planning_add' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<div class="modal-header border-secondary">
<h5 class="modal-title">Добавить в план</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" name="task_id" id="modalTaskId">
<div class="small text-muted mb-2" id="modalTaskTitle"></div>
<div class="mb-3">
<label class="form-label small text-muted">Станок</label>
<select class="form-select border-secondary" name="machine_id" required>
{% for m in machines %}
<option value="{{ m.id }}">{{ m.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-2">
<label class="form-label small text-muted">Сколько в план (шт)</label>
<input type="number" min="1" class="form-control border-secondary" name="quantity_plan" id="modalQty" required>
</div>
<div class="small text-muted" id="modalHint"></div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-outline-accent">Добавить</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('addToPlanModal');
if (!modal) return;
modal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
const taskId = btn.getAttribute('data-task-id');
const name = btn.getAttribute('data-task-name');
const rem = btn.getAttribute('data-task-rem');
document.getElementById('modalTaskId').value = taskId;
document.getElementById('modalTaskTitle').textContent = name;
document.getElementById('modalHint').textContent = rem !== null ? `Осталось: ${rem} шт` : '';
const qty = document.getElementById('modalQty');
qty.value = '';
if (rem && !isNaN(parseInt(rem, 10))) qty.max = Math.max(1, parseInt(rem, 10));
else qty.removeAttribute('max');
});
});
</script>
{% endblock %}

View File

@@ -135,6 +135,14 @@
<label class="form-label small text-muted">№ Сделки</label>
<input type="text" class="form-control border-secondary" id="dealNumber">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Статус</label>
<select class="form-select border-secondary" id="dealStatus">
<option value="lead">Зашла</option>
<option value="work" selected>В работе</option>
<option value="done">Завершена</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Компания</label>
<div class="d-flex gap-2">
@@ -311,6 +319,7 @@ document.addEventListener('DOMContentLoaded', function () {
const dealNumber = document.getElementById('dealNumber');
const dealCompany = document.getElementById('dealCompany');
const dealDescription = document.getElementById('dealDescription');
const dealStatus = document.getElementById('dealStatus');
const dealSaveBtn = document.getElementById('dealSaveBtn');
const companyModal = document.getElementById('companyModal');
@@ -362,7 +371,13 @@ document.addEventListener('DOMContentLoaded', function () {
if (fillFromPdf) fillFromPdf.disabled = !hasPdf;
}
if (drawingFile) drawingFile.addEventListener('change', updateFileButtons);
if (drawingFile) drawingFile.addEventListener('change', function () {
updateFileButtons();
if (!drawingName) return;
if (drawingName.value && drawingName.value.trim() !== '') return;
if (!drawingFile.files || drawingFile.files.length === 0) return;
drawingName.value = filenameBase(drawingFile.files[0].name);
});
if (extraDrawing) extraDrawing.addEventListener('change', updateFileButtons);
updateFileButtons();
@@ -459,6 +474,7 @@ document.addEventListener('DOMContentLoaded', function () {
dealNumber.value = '';
dealCompany.value = '';
dealDescription.value = '';
if (dealStatus) dealStatus.value = 'work';
if (mode === 'edit' && dealSelect && dealSelect.value) {
const data = await getJson(`/planning/deal/${dealSelect.value}/json/`);
@@ -466,6 +482,7 @@ document.addEventListener('DOMContentLoaded', function () {
dealNumber.value = data.number || '';
dealCompany.value = data.company_id ? String(data.company_id) : '';
dealDescription.value = data.description || '';
if (dealStatus) dealStatus.value = data.status || 'work';
}
});
}
@@ -476,6 +493,7 @@ document.addEventListener('DOMContentLoaded', function () {
id: dealId.value,
number: dealNumber.value,
company_id: dealCompany.value,
status: dealStatus ? dealStatus.value : 'work',
description: dealDescription.value,
};
const data = await postForm('{% url "deal_upsert" %}', payload);