Добавил страницу отгрузки, подправил логику генерации сменных заданий. Организовал редактирование позици сделок
All checks were successful
Deploy MES Core / deploy (push) Successful in 29s

This commit is contained in:
2026-04-14 07:27:54 +03:00
parent 69edd3fa97
commit 49e9080d0e
14 changed files with 2056 additions and 564 deletions

View File

@@ -6,6 +6,7 @@
<th data-sort-type="date">Дата</th>
<th>Сделка</th>
<th>Цех/Пост</th>
<th>Операция</th>
<th>Наименование</th>
<th>Материал</th>
<th data-sort="false" class="text-center">Файлы</th>
@@ -28,6 +29,9 @@
<span class="badge bg-secondary"></span>
{% endif %}
</td>
<td class="small">
{{ wi.operation.name|default:wi.stage|default:"—" }}
</td>
<td class="fw-bold">
{{ wi.entity.drawing_number|default:"—" }} {{ wi.entity.name }}
</td>

View File

@@ -40,7 +40,13 @@
</button>
</form>
{% endif %}
{% if user_role in 'admin,technologist' %}
{% if user_role in 'admin,clerk,manager,prod_head,technologist' %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'shipping' %}?deal_id={{ deal.id }}">
<i class="bi bi-truck me-1"></i>Отгрузка
</a>
{% endif %}
{% if user_role in 'admin,technologist,manager,prod_head' %}
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealItemModal">
<i class="bi bi-plus-lg me-1"></i>Добавить задание
</button>
@@ -65,7 +71,7 @@
<th data-sort="false" style="width: 160px;">Прогресс</th>
<th class="text-center">Заказано / Сделано / В плане</th>
<th class="text-center">Осталось</th>
<th data-sort="false" class="text-end">В производство</th>
<th data-sort="false" class="text-end">Действия</th>
</tr>
</thead>
<tbody>
@@ -88,20 +94,46 @@
</td>
<td class="text-center">{{ it.remaining_qty }}</td>
<td class="text-end" onclick="event.stopPropagation();">
{% if user_role in 'admin,technologist' %}
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#startProductionModal"
data-entity-id="{{ it.entity.id }}"
data-entity-label="{{ it.entity.drawing_number|default:'—' }} {{ it.entity.name }}"
>
<i class="bi bi-play-fill me-1"></i>В производство
</button>
{% else %}
<button type="button" class="btn btn-outline-secondary btn-sm" disabled>В производство</button>
{% endif %}
<div class="d-flex justify-content-end gap-1 flex-wrap" onclick="event.stopPropagation();">
{% if user_role in 'admin,technologist,manager,prod_head' %}
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#startProductionModal"
data-entity-id="{{ it.entity.id }}"
data-entity-label="{{ it.entity.drawing_number|default:'—' }} {{ it.entity.name }}"
>
<i class="bi bi-play-fill me-1"></i>В производство
</button>
<form method="post" action="{% url 'deal_item_upsert' %}" class="d-inline-flex gap-1 align-items-center" onclick="event.stopPropagation();">
{% csrf_token %}
<input type="hidden" name="action" value="set_qty">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="entity_id" value="{{ it.entity.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<input class="form-control form-control-sm bg-body text-body border-secondary" style="width:90px;" type="number" min="1" name="quantity" value="{{ it.quantity }}" title="Кол-во по сделке" required>
<button class="btn btn-outline-secondary btn-sm" type="submit" title="Обновить количество">OK</button>
</form>
<button
type="button"
class="btn btn-outline-danger btn-sm"
data-bs-toggle="modal"
data-bs-target="#dealItemDeleteModal"
data-deal-id="{{ deal.id }}"
data-entity-id="{{ it.entity.id }}"
data-next="{{ request.get_full_path }}"
data-entity-label="{{ it.entity.drawing_number|default:'—' }} {{ it.entity.name }}"
title="Удалить из сделки"
>
<i class="bi bi-trash"></i>
</button>
{% else %}
<button type="button" class="btn btn-outline-secondary btn-sm" disabled>В производство</button>
{% endif %}
</div>
</td>
</tr>
{% empty %}
@@ -182,6 +214,20 @@
</div>
</td>
<td class="text-end">
{% if user_role in 'admin,technologist' %}
{% if bi.started_qty and bi.started_qty > 0 %}
<button
type="button"
class="btn btn-outline-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#rollbackProductionModal{{ bi.id }}"
title="Откатить запуск в производство"
>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
{% endif %}
{% endif %}
{% if user_role in 'admin,technologist' and not b.is_default %}
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#dealBatchItemModal" data-batch-id="{{ b.id }}">Добавить</button>
<form method="post" action="{% url 'deal_batch_action' %}" class="d-inline">
@@ -202,6 +248,39 @@
{% else %}
<div class="text-muted">Пусто</div>
{% endif %}
{% for bi in b.items_list %}
{% if user_role in 'admin,technologist' and bi.started_qty and bi.started_qty > 0 %}
<div class="modal fade" id="rollbackProductionModal{{ bi.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'deal_batch_action' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="rollback_batch_item_production">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="item_id" value="{{ bi.id }}">
<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">
<div class="small text-muted mb-2">{{ bi.entity.drawing_number|default:"—" }} {{ bi.entity.name }}</div>
<div class="mb-3">
<label class="form-label">Сколько откатить, шт</label>
<input class="form-control bg-body text-body border-secondary" type="number" min="1" max="{{ bi.started_qty }}" name="quantity" value="{{ bi.started_qty }}" required>
<div class="form-text">Запущено в партии: {{ bi.started_qty }} шт</div>
</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-warning">Откатить</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endfor %}
</td>
<td class="text-end">
{% if user_role in 'admin,technologist' and not b.is_default %}
@@ -526,14 +605,19 @@
<div class="modal fade" id="dealItemModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form method="post" action="{% url 'deal_item_upsert' %}" class="modal-content border-secondary">
<form method="post" action="{% url 'deal_item_upsert' %}" class="modal-content border-secondary" id="dealItemForm">
{% csrf_token %}
<input type="hidden" name="action" value="add">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<input type="hidden" name="quantity" value="1">
<input type="hidden" name="entity_id" id="diEntityId" required>
<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">
<div class="row g-2 align-items-end">
<div class="col-md-3">
@@ -546,37 +630,60 @@
</div>
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" id="diDn" placeholder="Опционально">
<input class="form-control bg-body text-body border-secondary" id="diDn" autocomplete="off">
</div>
<div class="col-md-4">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" id="diName" placeholder="Напр. Основание">
<input class="form-control bg-body text-body border-secondary" id="diName" autocomplete="off">
</div>
<div class="col-md-2 d-grid">
<button type="button" class="btn btn-outline-secondary" id="diSearchBtn">Поиск</button>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-8">
<label class="form-label">Найдено</label>
<select class="form-select bg-body text-body border-secondary" id="diFound"></select>
<input type="hidden" name="entity_id" id="diEntityId" required>
</div>
<div class="col-md-4">
<label class="form-label">Кол-во, шт</label>
<input class="form-control bg-body text-body border-secondary" name="quantity" id="diQty" value="1" required>
</div>
<div class="small text-muted mt-2" id="diSearchStatus"></div>
<div class="mt-2">
<label class="form-label">Результаты</label>
<select class="form-select bg-body text-body border-secondary" id="diFound" size="8"></select>
<div class="form-text">Выбери строку и нажми «Добавить».</div>
</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="submit" class="btn btn-outline-accent">Добавить</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="dealItemDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'deal_item_upsert' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="delete">
<input type="hidden" name="deal_id" id="diDelDealId" value="">
<input type="hidden" name="entity_id" id="diDelEntityId" value="">
<input type="hidden" name="next" id="diDelNext" value="">
<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">
<div class="mb-2">Вы уверены?</div>
<div class="small text-muted" id="diDelLabel"></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-danger">Удалить</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('tr.deal-entity-row[data-href]').forEach(tr => {
@@ -593,6 +700,28 @@ document.addEventListener('DOMContentLoaded', () => {
const spQty = document.getElementById('spQty');
const spSubmit = document.getElementById('spSubmit');
const delModal = document.getElementById('dealItemDeleteModal');
const delDealId = document.getElementById('diDelDealId');
const delEntityId = document.getElementById('diDelEntityId');
const delNext = document.getElementById('diDelNext');
const delLabel = document.getElementById('diDelLabel');
if (delModal) {
delModal.addEventListener('shown.bs.modal', (event) => {
const btn = event.relatedTarget;
const dealId = btn ? (btn.getAttribute('data-deal-id') || '') : '';
const entityId = btn ? (btn.getAttribute('data-entity-id') || '') : '';
const nextUrl = btn ? (btn.getAttribute('data-next') || '') : '';
const label = btn ? (btn.getAttribute('data-entity-label') || '') : '';
if (delDealId) delDealId.value = dealId;
if (delEntityId) delEntityId.value = entityId;
if (delNext) delNext.value = nextUrl;
if (delLabel) delLabel.textContent = label;
});
}
function spApplyFilter(entityId) {
if (!spSelect) return;
let firstVisible = null;
@@ -642,6 +771,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (spQty) spQty.focus({ preventScroll: true });
});
}
});
document.addEventListener('DOMContentLoaded', () => {
@@ -685,7 +815,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', () => {
const modalEl = document.getElementById('dealItemModal');
const formEl = modalEl ? modalEl.querySelector('form') : null;
const formEl = document.getElementById('dealItemForm');
const typeEl = document.getElementById('diType');
const dnEl = document.getElementById('diDn');
@@ -693,24 +823,56 @@ document.addEventListener('DOMContentLoaded', () => {
const foundEl = document.getElementById('diFound');
const idEl = document.getElementById('diEntityId');
const btn = document.getElementById('diSearchBtn');
const statusEl = document.getElementById('diSearchStatus');
if (!typeEl || !dnEl || !nameEl || !foundEl || !idEl) return;
if (!modalEl || !formEl || !typeEl || !dnEl || !nameEl || !foundEl || !idEl || !btn || !statusEl) return;
function setStatus(text) {
statusEl.textContent = text || '';
}
function setSelectedFromFound() {
idEl.value = foundEl.value || '';
}
async function search(opts = { focusFound: false }) {
async function runSearch() {
const params = new URLSearchParams({
entity_type: (typeEl.value || ''),
q_dn: (dnEl.value || ''),
q_name: (nameEl.value || ''),
});
const res = await fetch('{% url "entities_search" %}?' + params.toString(), { credentials: 'same-origin' });
const data = await res.json();
const items = (data && data.results) || [];
setStatus('Поиск...');
foundEl.innerHTML = '';
idEl.value = '';
let res;
try {
res = await fetch('{% url "entities_search" %}?' + params.toString(), { credentials: 'same-origin' });
} catch (_) {
setStatus('Ошибка сети при поиске.');
return;
}
if (!res.ok) {
setStatus(`Ошибка поиска: ${res.status}`);
return;
}
let data;
try {
data = await res.json();
} catch (_) {
setStatus('Ошибка: сервер вернул не JSON.');
return;
}
if (data && data.error) {
setStatus(`Ошибка поиска: ${data.error}`);
return;
}
const items = (data && data.results) || [];
items.forEach(it => {
const opt = document.createElement('option');
opt.value = String(it.id);
@@ -718,50 +880,42 @@ document.addEventListener('DOMContentLoaded', () => {
foundEl.appendChild(opt);
});
const count = (data && typeof data.count === 'number') ? data.count : items.length;
if (items.length) {
foundEl.value = String(items[0].id);
setSelectedFromFound();
if (opts && opts.focusFound) {
foundEl.focus({ preventScroll: true });
}
setStatus(`Найдено: ${count}`);
foundEl.focus({ preventScroll: true });
} else {
idEl.value = '';
setStatus(`Ничего не найдено (0).`);
}
foundEl.onchange = setSelectedFromFound;
return items;
}
if (btn) btn.onclick = () => { search({ focusFound: true }); };
btn.addEventListener('click', () => runSearch());
foundEl.addEventListener('change', () => setSelectedFromFound());
const onEnterSearch = (e) => {
if (e.key !== 'Enter') return;
e.preventDefault();
search({ focusFound: true });
runSearch();
};
dnEl.addEventListener('keydown', onEnterSearch);
nameEl.addEventListener('keydown', onEnterSearch);
foundEl.addEventListener('keydown', (e) => {
if (e.key !== 'Enter') return;
e.preventDefault();
formEl.addEventListener('submit', (e) => {
setSelectedFromFound();
if (idEl.value && formEl) {
formEl.requestSubmit();
} else {
search({ focusFound: true });
if (!idEl.value) {
e.preventDefault();
setStatus('Выбери позицию из результатов поиска.');
}
});
if (modalEl) {
modalEl.addEventListener('shown.bs.modal', () => {
setTimeout(() => {
dnEl.focus({ preventScroll: true });
dnEl.select();
}, 0);
});
}
modalEl.addEventListener('shown.bs.modal', () => {
setStatus('');
dnEl.focus({ preventScroll: true });
dnEl.select();
});
});
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('tr.task-row[data-href]').forEach(function (row) {
@@ -889,5 +1043,7 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
});
</script>
{% endblock %}

View File

@@ -25,72 +25,33 @@
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data" id="product-info-form">
{% csrf_token %}
<input type="hidden" name="action" value="save">
<input type="hidden" name="next" value="{{ next }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<div class="row g-2">
<div class="col-md-4">
<div class="col-md-2">
<label class="form-label">Тип</label>
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
<div class="mt-1"><span class="badge bg-secondary">{{ entity.get_entity_type_display }}</span></div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<label class="form-label">Заполнен</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
<label class="form-check-label" for="pf">Заполнено</label>
</div>
</div>
<div class="col-12">
<div class="col-md-5">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-6">
<label class="form-label">Чертёж (PDF)</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
{% if entity.pdf_main %}
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
{% if not can_edit %}
<div class="col-md-6">
<label class="form-label">Техпроцесс</label>
<div class="form-control bg-body text-body border-secondary">
{% if entity_ops %}
{% for eo in entity_ops %}
{{ eo.seq }}. {{ eo.operation.name }}{% if eo.operation.workshop %} ({{ eo.operation.workshop.name }}){% endif %}{% if not forloop.last %} · {% endif %}
{% endfor %}
{% else %}
— не указан —
{% endif %}
</div>
</div>
{% endif %}
<div class="col-md-3">
<label class="form-label">Сварка</label>
<div class="col-md-2">
<label class="form-label">Заполнен</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="requires_welding" id="rw" {% if passport and passport.requires_welding %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
<label class="form-check-label" for="rw">Требуется сварка</label>
</div>
</div>
<div class="col-md-3">
<label class="form-label">Покраска</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="requires_painting" id="rp" {% if passport and passport.requires_painting %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
<label class="form-check-label" for="rp">Требуется покраска</label>
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
<label class="form-check-label" for="pf">Заполнено</label>
</div>
</div>
@@ -114,6 +75,45 @@
<input class="form-control bg-body text-body border-secondary" name="coating_area_m2" value="{% if passport and passport.coating_area_m2 %}{{ passport.coating_area_m2 }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<label class="form-label">Чертёж (PDF)</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
{% if entity.pdf_main %}
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">DXF/IGES/STEP</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
{% if entity.dxf_file %}
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">Картинка</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
{% if entity.preview %}
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
{% endif %}
</div>
{% if not can_edit %}
<div class="col-12">
<label class="form-label">Техпроцесс</label>
<div class="form-control bg-body text-body border-secondary">
{% if entity_ops %}
{% for eo in entity_ops %}
{{ eo.seq }}. {{ eo.operation.name }}{% if eo.operation.workshop %} ({{ eo.operation.workshop.name }}){% endif %}{% if not forloop.last %} · {% endif %}
{% endfor %}
{% else %}
— не указан —
{% endif %}
</div>
</div>
{% endif %}
<div class="col-12">
<label class="form-label">Технические требования</label>
<textarea class="form-control bg-body text-body border-secondary" name="technical_requirements" rows="4" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.technical_requirements }}{% endif %}</textarea>
@@ -128,7 +128,7 @@
</form>
{% if can_edit %}
<div class="mt-3">
<div class="mt-3" id="techprocess-editor" data-form-id="product-info-form">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
@@ -137,69 +137,154 @@
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></th>
<th style="width:220px;" class="text-end" data-sort="false">Действия</th>
</tr>
</thead>
<tbody>
{% for eo in entity_ops %}
<tr>
<td class="text-muted">{{ eo.seq }}</td>
<td class="fw-bold">{{ eo.operation.name }}</td>
<td class="small text-muted">{% if eo.operation.workshop %}{{ eo.operation.workshop.name }}{% else %}—{% endif %}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="up">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="down">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="delete_entity_operation">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit">Удалить</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-3">Операции не добавлены</td></tr>
{% endfor %}
<tbody id="tpRows">
{% if selected_operation_ids %}
{% for op_id in selected_operation_ids %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}" {% if op.id == op_id %}selected{% endif %}>{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
{% for i in '1234'|make_list %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
<form method="post" action="{% url 'product_info' entity.id %}" class="row g-2 mt-2">
{% csrf_token %}
<input type="hidden" name="action" value="add_entity_operation">
<input type="hidden" name="next" value="{{ next }}">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-2">
<button type="button" class="btn btn-outline-accent btn-sm" id="tpAddRow">+ строка</button>
<button class="btn btn-outline-accent" type="submit" form="product-info-form">Сохранить</button>
</div>
<div class="col-md-10">
<label class="form-label">Добавить операцию</label>
<select class="form-select bg-body text-body border-secondary" name="operation_id">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end justify-content-end">
<button class="btn btn-outline-accent" type="submit">+</button>
</div>
</form>
<div id="tpHidden"></div>
<template id="tpRowTemplate">
<tr class="tp-row">
<td class="text-muted tp-idx"></td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
</template>
<script>
(function() {
const root = document.getElementById('techprocess-editor');
if (!root) return;
const formId = root.getAttribute('data-form-id') || 'product-info-form';
const form = document.getElementById(formId);
if (!form) return;
const tbody = document.getElementById('tpRows');
const addBtn = document.getElementById('tpAddRow');
const hidden = document.getElementById('tpHidden');
const tpl = document.getElementById('tpRowTemplate');
function renumber() {
tbody.querySelectorAll('.tp-row').forEach((tr, idx) => {
const cell = tr.querySelector('.tp-idx');
if (cell) cell.textContent = String(idx + 1);
});
}
function bindRow(tr) {
tr.querySelector('.tp-up')?.addEventListener('click', () => {
const prev = tr.previousElementSibling;
if (prev) tbody.insertBefore(tr, prev);
renumber();
});
tr.querySelector('.tp-down')?.addEventListener('click', () => {
const next = tr.nextElementSibling;
if (next) tbody.insertBefore(next, tr);
renumber();
});
tr.querySelector('.tp-del')?.addEventListener('click', () => {
tr.remove();
renumber();
});
}
tbody.querySelectorAll('.tp-row').forEach(bindRow);
renumber();
addBtn?.addEventListener('click', () => {
const frag = tpl.content.cloneNode(true);
const tr = frag.querySelector('tr');
tbody.appendChild(frag);
if (tr) bindRow(tr);
renumber();
});
form.addEventListener('submit', () => {
hidden.innerHTML = '';
const values = [];
tbody.querySelectorAll('.tp-select').forEach(sel => {
const v = (sel.value || '').toString().trim();
if (!v) return;
if (values.includes(v)) return;
values.push(v);
});
values.forEach(v => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'operation_ids';
inp.value = v;
inp.setAttribute('form', formId);
hidden.appendChild(inp);
});
});
})();
</script>
</div>
{% endif %}
@@ -290,6 +375,7 @@
<th>Тип</th>
<th>Обозначение</th>
<th>Наименование</th>
<th class="text-center" style="width:120px;">Заполнено</th>
<th class="text-center">Кол-во</th>
<th data-sort="false" class="text-end"></th>
</tr>
@@ -300,6 +386,13 @@
<td class="small text-muted">{{ ln.child.get_entity_type_display }}</td>
<td class="fw-bold">{{ ln.child.drawing_number|default:"—" }}</td>
<td>{{ ln.child.name }}</td>
<td class="text-center">
{% if ln.child.passport_filled %}
<span class="badge bg-success">Да</span>
{% else %}
<span class="badge bg-secondary">Нет</span>
{% endif %}
</td>
<td class="text-center" style="max-width:220px;" onclick="event.stopPropagation();">
<form method="post" action="{% url 'product_info' entity.id %}" class="d-flex gap-2 align-items-center justify-content-center" onclick="event.stopPropagation();">
{% csrf_token %}
@@ -325,7 +418,7 @@
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Пока нет компонентов</td></tr>
<tr><td colspan="6" class="text-center text-muted py-4">Пока нет компонентов</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -25,24 +25,29 @@
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data" id="product-info-form">
{% csrf_token %}
<input type="hidden" name="action" value="save">
<input type="hidden" name="next" value="{{ next }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<div class="row g-2">
<div class="col-md-4">
<div class="col-md-2">
<label class="form-label">Тип</label>
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
<div class="mt-1"><span class="badge bg-secondary">{{ entity.get_entity_type_display }}</span></div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<div class="col-md-5">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-2">
<label class="form-label">Заполнен</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
@@ -50,11 +55,6 @@
</div>
</div>
<div class="col-12">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-6">
<label class="form-label">Материал литья</label>
<input class="form-control bg-body text-body border-secondary" name="casting_material" value="{% if passport %}{{ passport.casting_material }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
@@ -80,7 +80,7 @@
</div>
{% endif %}
<div class="col-md-6">
<div class="col-md-4">
<label class="form-label">Чертёж (PDF)</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
{% if entity.pdf_main %}
@@ -88,7 +88,15 @@
{% endif %}
</div>
<div class="col-md-6">
<div class="col-md-4">
<label class="form-label">DXF/IGES/STEP</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
{% if entity.dxf_file %}
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">Картинка</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
{% if entity.preview %}
@@ -105,7 +113,7 @@
</form>
{% if can_edit %}
<div class="mt-3">
<div class="mt-3" id="techprocess-editor" data-form-id="product-info-form">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
@@ -114,69 +122,154 @@
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></th>
<th style="width:220px;" class="text-end" data-sort="false">Действия</th>
</tr>
</thead>
<tbody>
{% for eo in entity_ops %}
<tr>
<td class="text-muted">{{ eo.seq }}</td>
<td class="fw-bold">{{ eo.operation.name }}</td>
<td class="small text-muted">{% if eo.operation.workshop %}{{ eo.operation.workshop.name }}{% else %}—{% endif %}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="up">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="down">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="delete_entity_operation">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit">Удалить</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-3">Операции не добавлены</td></tr>
{% endfor %}
<tbody id="tpRows">
{% if selected_operation_ids %}
{% for op_id in selected_operation_ids %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}" {% if op.id == op_id %}selected{% endif %}>{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
{% for i in '1234'|make_list %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
<form method="post" action="{% url 'product_info' entity.id %}" class="row g-2 mt-2">
{% csrf_token %}
<input type="hidden" name="action" value="add_entity_operation">
<input type="hidden" name="next" value="{{ next }}">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-2">
<button type="button" class="btn btn-outline-accent btn-sm" id="tpAddRow">+ строка</button>
<button class="btn btn-outline-accent" type="submit" form="product-info-form">Сохранить</button>
</div>
<div class="col-md-10">
<label class="form-label">Добавить операцию</label>
<select class="form-select bg-body text-body border-secondary" name="operation_id">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end justify-content-end">
<button class="btn btn-outline-accent" type="submit">+</button>
</div>
</form>
<div id="tpHidden"></div>
<template id="tpRowTemplate">
<tr class="tp-row">
<td class="text-muted tp-idx"></td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
</template>
<script>
(function() {
const root = document.getElementById('techprocess-editor');
if (!root) return;
const formId = root.getAttribute('data-form-id') || 'product-info-form';
const form = document.getElementById(formId);
if (!form) return;
const tbody = document.getElementById('tpRows');
const addBtn = document.getElementById('tpAddRow');
const hidden = document.getElementById('tpHidden');
const tpl = document.getElementById('tpRowTemplate');
function renumber() {
tbody.querySelectorAll('.tp-row').forEach((tr, idx) => {
const cell = tr.querySelector('.tp-idx');
if (cell) cell.textContent = String(idx + 1);
});
}
function bindRow(tr) {
tr.querySelector('.tp-up')?.addEventListener('click', () => {
const prev = tr.previousElementSibling;
if (prev) tbody.insertBefore(tr, prev);
renumber();
});
tr.querySelector('.tp-down')?.addEventListener('click', () => {
const next = tr.nextElementSibling;
if (next) tbody.insertBefore(next, tr);
renumber();
});
tr.querySelector('.tp-del')?.addEventListener('click', () => {
tr.remove();
renumber();
});
}
tbody.querySelectorAll('.tp-row').forEach(bindRow);
renumber();
addBtn?.addEventListener('click', () => {
const frag = tpl.content.cloneNode(true);
const tr = frag.querySelector('tr');
tbody.appendChild(frag);
if (tr) bindRow(tr);
renumber();
});
form.addEventListener('submit', () => {
hidden.innerHTML = '';
const values = [];
tbody.querySelectorAll('.tp-select').forEach(sel => {
const v = (sel.value || '').toString().trim();
if (!v) return;
if (values.includes(v)) return;
values.push(v);
});
values.forEach(v => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'operation_ids';
inp.value = v;
inp.setAttribute('form', formId);
hidden.appendChild(inp);
});
});
})();
</script>
</div>
{% endif %}
</div>

View File

@@ -25,24 +25,29 @@
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data" id="product-info-form">
{% csrf_token %}
<input type="hidden" name="action" value="save">
<input type="hidden" name="next" value="{{ next }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<div class="row g-2">
<div class="col-md-4">
<div class="col-md-2">
<label class="form-label">Тип</label>
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
<div class="mt-1"><span class="badge bg-secondary">{{ entity.get_entity_type_display }}</span></div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<div class="col-md-5">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-2">
<label class="form-label">Заполнен</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
@@ -50,13 +55,32 @@
</div>
</div>
<div class="col-12">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
<div class="col-md-4">
<label class="form-label">Чертёж/ТЗ (PDF)</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
{% if entity.pdf_main %}
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">DXF/IGES/STEP</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
{% if entity.dxf_file %}
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">Картинка</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
{% if entity.preview %}
<div class="small mt-1"><a href="{{ entity.preview.url }}" target="_blank">Открыть текущую</a></div>
{% endif %}
</div>
{% if not can_edit %}
<div class="col-md-6">
<div class="col-12">
<label class="form-label">Техпроцесс</label>
<div class="form-control bg-body text-body border-secondary">
{% if entity_ops %}
@@ -70,12 +94,9 @@
</div>
{% endif %}
<div class="col-md-6">
<label class="form-label">Чертёж/ТЗ (PDF)</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
{% if entity.pdf_main %}
<div class="small mt-1"><a href="{{ entity.pdf_main.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
<div class="col-12">
<label class="form-label">Пояснения</label>
<textarea class="form-control bg-body text-body border-secondary" name="notes" rows="3" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.notes }}{% endif %}</textarea>
</div>
<div class="col-12">
@@ -83,11 +104,6 @@
<textarea class="form-control bg-body text-body border-secondary" name="technical_requirements" rows="4" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.technical_requirements }}{% endif %}</textarea>
</div>
<div class="col-12">
<label class="form-label">Пояснения</label>
<textarea class="form-control bg-body text-body border-secondary" name="notes" rows="3" {% if not can_edit %}disabled{% endif %}>{% if passport %}{{ passport.notes }}{% endif %}</textarea>
</div>
<div class="col-12 d-flex justify-content-end mt-2">
{% if can_edit %}
<button class="btn btn-outline-accent" type="submit">Сохранить</button>
@@ -97,7 +113,7 @@
</form>
{% if can_edit %}
<div class="mt-3">
<div class="mt-3" id="techprocess-editor" data-form-id="product-info-form">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
@@ -106,69 +122,154 @@
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></th>
<th style="width:220px;" class="text-end" data-sort="false">Действия</th>
</tr>
</thead>
<tbody>
{% for eo in entity_ops %}
<tr>
<td class="text-muted">{{ eo.seq }}</td>
<td class="fw-bold">{{ eo.operation.name }}</td>
<td class="small text-muted">{% if eo.operation.workshop %}{{ eo.operation.workshop.name }}{% else %}—{% endif %}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="up">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="down">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="delete_entity_operation">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit">Удалить</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-3">Операции не добавлены</td></tr>
{% endfor %}
<tbody id="tpRows">
{% if selected_operation_ids %}
{% for op_id in selected_operation_ids %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}" {% if op.id == op_id %}selected{% endif %}>{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
{% for i in '1234'|make_list %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
<form method="post" action="{% url 'product_info' entity.id %}" class="row g-2 mt-2">
{% csrf_token %}
<input type="hidden" name="action" value="add_entity_operation">
<input type="hidden" name="next" value="{{ next }}">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-2">
<button type="button" class="btn btn-outline-accent btn-sm" id="tpAddRow">+ строка</button>
<button class="btn btn-outline-accent" type="submit" form="product-info-form">Сохранить</button>
</div>
<div class="col-md-10">
<label class="form-label">Добавить операцию</label>
<select class="form-select bg-body text-body border-secondary" name="operation_id">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end justify-content-end">
<button class="btn btn-outline-accent" type="submit">+</button>
</div>
</form>
<div id="tpHidden"></div>
<template id="tpRowTemplate">
<tr class="tp-row">
<td class="text-muted tp-idx"></td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
</template>
<script>
(function() {
const root = document.getElementById('techprocess-editor');
if (!root) return;
const formId = root.getAttribute('data-form-id') || 'product-info-form';
const form = document.getElementById(formId);
if (!form) return;
const tbody = document.getElementById('tpRows');
const addBtn = document.getElementById('tpAddRow');
const hidden = document.getElementById('tpHidden');
const tpl = document.getElementById('tpRowTemplate');
function renumber() {
tbody.querySelectorAll('.tp-row').forEach((tr, idx) => {
const cell = tr.querySelector('.tp-idx');
if (cell) cell.textContent = String(idx + 1);
});
}
function bindRow(tr) {
tr.querySelector('.tp-up')?.addEventListener('click', () => {
const prev = tr.previousElementSibling;
if (prev) tbody.insertBefore(tr, prev);
renumber();
});
tr.querySelector('.tp-down')?.addEventListener('click', () => {
const next = tr.nextElementSibling;
if (next) tbody.insertBefore(next, tr);
renumber();
});
tr.querySelector('.tp-del')?.addEventListener('click', () => {
tr.remove();
renumber();
});
}
tbody.querySelectorAll('.tp-row').forEach(bindRow);
renumber();
addBtn?.addEventListener('click', () => {
const frag = tpl.content.cloneNode(true);
const tr = frag.querySelector('tr');
tbody.appendChild(frag);
if (tr) bindRow(tr);
renumber();
});
form.addEventListener('submit', () => {
hidden.innerHTML = '';
const values = [];
tbody.querySelectorAll('.tp-select').forEach(sel => {
const v = (sel.value || '').toString().trim();
if (!v) return;
if (values.includes(v)) return;
values.push(v);
});
values.forEach(v => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'operation_ids';
inp.value = v;
inp.setAttribute('form', formId);
hidden.appendChild(inp);
});
});
})();
</script>
</div>
{% endif %}
</div>

View File

@@ -25,24 +25,29 @@
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data" id="product-info-form">
{% csrf_token %}
<input type="hidden" name="action" value="save">
<input type="hidden" name="next" value="{{ next }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<div class="row g-2">
<div class="col-md-4">
<div class="col-md-2">
<label class="form-label">Тип</label>
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
<div class="mt-1"><span class="badge bg-secondary">{{ entity.get_entity_type_display }}</span></div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<div class="col-md-5">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-2">
<label class="form-label">Заполнен</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
@@ -50,11 +55,6 @@
</div>
</div>
<div class="col-12">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-6">
<label class="form-label">Материал заготовки</label>
<select class="form-select bg-body text-body border-secondary" name="planned_material_id" {% if not can_edit %}disabled{% endif %}>
@@ -65,21 +65,6 @@
</select>
</div>
{% if not can_edit %}
<div class="col-md-6">
<label class="form-label">Техпроцесс</label>
<div class="form-control bg-body text-body border-secondary">
{% if entity_ops %}
{% for eo in entity_ops %}
{{ eo.seq }}. {{ eo.operation.name }}{% if eo.operation.workshop %} ({{ eo.operation.workshop.name }}){% endif %}{% if not forloop.last %} · {% endif %}
{% endfor %}
{% else %}
— не указан —
{% endif %}
</div>
</div>
{% endif %}
<div class="col-md-3">
<label class="form-label">Толщина, мм</label>
<input class="form-control bg-body text-body border-secondary" name="thickness_mm" value="{% if passport and passport.thickness_mm %}{{ passport.thickness_mm }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
@@ -105,7 +90,22 @@
<input class="form-control bg-body text-body border-secondary" name="pierce_count" value="{% if passport and passport.pierce_count %}{{ passport.pierce_count }}{% endif %}" inputmode="numeric" {% if not can_edit %}disabled{% endif %}>
</div>
{% if not can_edit %}
<div class="col-md-3">
<label class="form-label">Техпроцесс</label>
<div class="form-control bg-body text-body border-secondary">
{% if entity_ops %}
{% for eo in entity_ops %}
{{ eo.seq }}. {{ eo.operation.name }}{% if eo.operation.workshop %} ({{ eo.operation.workshop.name }}){% endif %}{% if not forloop.last %} · {% endif %}
{% endfor %}
{% else %}
— не указан —
{% endif %}
</div>
</div>
{% endif %}
<div class="col-md-4">
<label class="form-label">Чертёж (PDF)</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
{% if entity.pdf_main %}
@@ -113,15 +113,15 @@
{% endif %}
</div>
<div class="col-md-3">
<div class="col-md-4">
<label class="form-label">DXF/IGES/STEP</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" {% if not can_edit %}disabled{% endif %}>
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
{% if entity.dxf_file %}
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
<div class="col-md-3">
<div class="col-md-4">
<label class="form-label">Картинка</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
{% if entity.preview %}
@@ -148,7 +148,7 @@
</form>
{% if can_edit %}
<div class="mt-3">
<div class="mt-3" id="techprocess-editor" data-form-id="product-info-form">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
@@ -157,69 +157,154 @@
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></th>
<th style="width:220px;" class="text-end" data-sort="false">Действия</th>
</tr>
</thead>
<tbody>
{% for eo in entity_ops %}
<tr>
<td class="text-muted">{{ eo.seq }}</td>
<td class="fw-bold">{{ eo.operation.name }}</td>
<td class="small text-muted">{% if eo.operation.workshop %}{{ eo.operation.workshop.name }}{% else %}—{% endif %}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="up">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="down">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="delete_entity_operation">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit">Удалить</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-3">Операции не добавлены</td></tr>
{% endfor %}
<tbody id="tpRows">
{% if selected_operation_ids %}
{% for op_id in selected_operation_ids %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}" {% if op.id == op_id %}selected{% endif %}>{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
{% for i in '1234'|make_list %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
<form method="post" action="{% url 'product_info' entity.id %}" class="row g-2 mt-2">
{% csrf_token %}
<input type="hidden" name="action" value="add_entity_operation">
<input type="hidden" name="next" value="{{ next }}">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-2">
<button type="button" class="btn btn-outline-accent btn-sm" id="tpAddRow">+ строка</button>
<button class="btn btn-outline-accent" type="submit" form="product-info-form">Сохранить</button>
</div>
<div class="col-md-10">
<label class="form-label">Добавить операцию</label>
<select class="form-select bg-body text-body border-secondary" name="operation_id">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end justify-content-end">
<button class="btn btn-outline-accent" type="submit">+</button>
</div>
</form>
<div id="tpHidden"></div>
<template id="tpRowTemplate">
<tr class="tp-row">
<td class="text-muted tp-idx"></td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
</template>
<script>
(function() {
const root = document.getElementById('techprocess-editor');
if (!root) return;
const formId = root.getAttribute('data-form-id') || 'product-info-form';
const form = document.getElementById(formId);
if (!form) return;
const tbody = document.getElementById('tpRows');
const addBtn = document.getElementById('tpAddRow');
const hidden = document.getElementById('tpHidden');
const tpl = document.getElementById('tpRowTemplate');
function renumber() {
tbody.querySelectorAll('.tp-row').forEach((tr, idx) => {
const cell = tr.querySelector('.tp-idx');
if (cell) cell.textContent = String(idx + 1);
});
}
function bindRow(tr) {
tr.querySelector('.tp-up')?.addEventListener('click', () => {
const prev = tr.previousElementSibling;
if (prev) tbody.insertBefore(tr, prev);
renumber();
});
tr.querySelector('.tp-down')?.addEventListener('click', () => {
const next = tr.nextElementSibling;
if (next) tbody.insertBefore(next, tr);
renumber();
});
tr.querySelector('.tp-del')?.addEventListener('click', () => {
tr.remove();
renumber();
});
}
tbody.querySelectorAll('.tp-row').forEach(bindRow);
renumber();
addBtn?.addEventListener('click', () => {
const frag = tpl.content.cloneNode(true);
const tr = frag.querySelector('tr');
tbody.appendChild(frag);
if (tr) bindRow(tr);
renumber();
});
form.addEventListener('submit', () => {
hidden.innerHTML = '';
const values = [];
tbody.querySelectorAll('.tp-select').forEach(sel => {
const v = (sel.value || '').toString().trim();
if (!v) return;
if (values.includes(v)) return;
values.push(v);
});
values.forEach(v => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'operation_ids';
inp.value = v;
inp.setAttribute('form', formId);
hidden.appendChild(inp);
});
});
})();
</script>
</div>
{% endif %}
</div>

View File

@@ -25,24 +25,29 @@
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data" id="product-info-form">
{% csrf_token %}
<input type="hidden" name="action" value="save">
<input type="hidden" name="next" value="{{ next }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<div class="row g-2">
<div class="col-md-4">
<div class="col-md-2">
<label class="form-label">Тип</label>
<input class="form-control bg-body text-body border-secondary" value="{{ entity.get_entity_type_display }}" disabled>
<div class="mt-1"><span class="badge bg-secondary">{{ entity.get_entity_type_display }}</span></div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" name="drawing_number" value="{{ entity.drawing_number }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<div class="col-md-5">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-2">
<label class="form-label">Заполнен</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="passport_filled" id="pf" {% if entity.passport_filled %}checked{% endif %} {% if not can_edit %}disabled{% endif %}>
@@ -50,11 +55,6 @@
</div>
</div>
<div class="col-12">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-6">
<label class="form-label">ГОСТ/ТУ</label>
<input class="form-control bg-body text-body border-secondary" name="gost" value="{% if passport %}{{ passport.gost }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
@@ -75,7 +75,7 @@
</div>
{% endif %}
<div class="col-md-6">
<div class="col-md-4">
<label class="form-label">Чертёж/паспорт (PDF)</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="pdf_main" accept="application/pdf" {% if not can_edit %}disabled{% endif %}>
{% if entity.pdf_main %}
@@ -83,7 +83,15 @@
{% endif %}
</div>
<div class="col-md-6">
<div class="col-md-4">
<label class="form-label">DXF/IGES/STEP</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="dxf_file" accept=".dxf,.iges,.igs,.step,.stp" {% if not can_edit %}disabled{% endif %}>
{% if entity.dxf_file %}
<div class="small mt-1"><a href="{{ entity.dxf_file.url }}" target="_blank">Открыть текущий</a></div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">Картинка</label>
<input class="form-control bg-body text-body border-secondary" type="file" name="preview" accept="image/*" {% if not can_edit %}disabled{% endif %}>
{% if entity.preview %}
@@ -100,7 +108,7 @@
</form>
{% if can_edit %}
<div class="mt-3">
<div class="mt-3" id="techprocess-editor" data-form-id="product-info-form">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
@@ -109,69 +117,154 @@
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></th>
<th style="width:220px;" class="text-end" data-sort="false">Действия</th>
</tr>
</thead>
<tbody>
{% for eo in entity_ops %}
<tr>
<td class="text-muted">{{ eo.seq }}</td>
<td class="fw-bold">{{ eo.operation.name }}</td>
<td class="small text-muted">{% if eo.operation.workshop %}{{ eo.operation.workshop.name }}{% else %}—{% endif %}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="up">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="move_entity_operation">
<input type="hidden" name="direction" value="down">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit"></button>
</form>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="delete_entity_operation">
<input type="hidden" name="entity_operation_id" value="{{ eo.id }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary" type="submit">Удалить</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-3">Операции не добавлены</td></tr>
{% endfor %}
<tbody id="tpRows">
{% if selected_operation_ids %}
{% for op_id in selected_operation_ids %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}" {% if op.id == op_id %}selected{% endif %}>{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
{% for i in '1234'|make_list %}
<tr class="tp-row">
<td class="text-muted tp-idx">{{ forloop.counter }}</td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
<form method="post" action="{% url 'product_info' entity.id %}" class="row g-2 mt-2">
{% csrf_token %}
<input type="hidden" name="action" value="add_entity_operation">
<input type="hidden" name="next" value="{{ next }}">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-2">
<button type="button" class="btn btn-outline-accent btn-sm" id="tpAddRow">+ строка</button>
<button class="btn btn-outline-accent" type="submit" form="product-info-form">Сохранить</button>
</div>
<div class="col-md-10">
<label class="form-label">Добавить операцию</label>
<select class="form-select bg-body text-body border-secondary" name="operation_id">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end justify-content-end">
<button class="btn btn-outline-accent" type="submit">+</button>
</div>
</form>
<div id="tpHidden"></div>
<template id="tpRowTemplate">
<tr class="tp-row">
<td class="text-muted tp-idx"></td>
<td>
<select class="form-select bg-body text-body border-secondary tp-select">
<option value="">— выбери —</option>
{% for op in operations %}
<option value="{{ op.id }}">{{ op.name }}{% if op.workshop %} ({{ op.workshop.name }}){% endif %}</option>
{% endfor %}
</select>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary tp-up"></button>
<button type="button" class="btn btn-outline-secondary tp-down"></button>
<button type="button" class="btn btn-outline-secondary tp-del">Удалить</button>
</div>
</td>
</tr>
</template>
<script>
(function() {
const root = document.getElementById('techprocess-editor');
if (!root) return;
const formId = root.getAttribute('data-form-id') || 'product-info-form';
const form = document.getElementById(formId);
if (!form) return;
const tbody = document.getElementById('tpRows');
const addBtn = document.getElementById('tpAddRow');
const hidden = document.getElementById('tpHidden');
const tpl = document.getElementById('tpRowTemplate');
function renumber() {
tbody.querySelectorAll('.tp-row').forEach((tr, idx) => {
const cell = tr.querySelector('.tp-idx');
if (cell) cell.textContent = String(idx + 1);
});
}
function bindRow(tr) {
tr.querySelector('.tp-up')?.addEventListener('click', () => {
const prev = tr.previousElementSibling;
if (prev) tbody.insertBefore(tr, prev);
renumber();
});
tr.querySelector('.tp-down')?.addEventListener('click', () => {
const next = tr.nextElementSibling;
if (next) tbody.insertBefore(next, tr);
renumber();
});
tr.querySelector('.tp-del')?.addEventListener('click', () => {
tr.remove();
renumber();
});
}
tbody.querySelectorAll('.tp-row').forEach(bindRow);
renumber();
addBtn?.addEventListener('click', () => {
const frag = tpl.content.cloneNode(true);
const tr = frag.querySelector('tr');
tbody.appendChild(frag);
if (tr) bindRow(tr);
renumber();
});
form.addEventListener('submit', () => {
hidden.innerHTML = '';
const values = [];
tbody.querySelectorAll('.tp-select').forEach(sel => {
const v = (sel.value || '').toString().trim();
if (!v) return;
if (values.includes(v)) return;
values.push(v);
});
values.forEach(v => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'operation_ids';
inp.value = v;
inp.setAttribute('form', formId);
hidden.appendChild(inp);
});
});
})();
</script>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,182 @@
{% 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">
<h3 class="text-accent mb-0"><i class="bi bi-truck me-2"></i>Отгрузка</h3>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}"><i class="bi bi-arrow-left me-1"></i>Назад</a>
</div>
<div class="card-body">
<form method="get" class="row g-2 align-items-end">
<div class="col-md-10">
<label class="form-label">Сделка</label>
<select class="form-select bg-body text-body border-secondary" name="deal_id">
<option value="">— выбери —</option>
{% for d in deals %}
<option value="{{ d.id }}" {% if selected_deal_id == d.id %}selected{% endif %}>
№{{ d.number }}{% if d.company %} · {{ d.company.name }}{% endif %}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-outline-accent" type="submit">Показать</button>
</div>
</form>
{% if selected_deal_id %}
<hr class="border-secondary my-4">
<form method="post" id="shipForm">
{% csrf_token %}
<input type="hidden" name="deal_id" value="{{ selected_deal_id }}">
<div class="fw-bold mb-2">Позиции к отгрузке (по сделке)</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr class="table-custom-header">
<th>Позиция</th>
<th class="text-center" style="width:140px;">Есть на складе</th>
<th class="text-center" style="width:180px;">К отгрузке</th>
</tr>
</thead>
<tbody>
{% for r in entity_rows %}
<tr>
<td>
<div class="fw-bold">{{ r.entity.drawing_number|default:"—" }} {{ r.entity.name }}</div>
<div class="small text-muted">{{ r.entity.get_entity_type_display }}</div>
</td>
<td class="text-center">{{ r.available }}</td>
<td class="text-center">
<input
class="form-control bg-body text-body border-secondary ship-qty"
type="number"
min="0"
step="1"
name="ent_{{ r.entity.id }}"
value="0"
data-label="{{ r.entity.drawing_number|default:'—' }} {{ r.entity.name }}"
>
</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center text-muted py-4">Позиции сделки не найдены</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if material_rows %}
<div class="fw-bold mt-4 mb-2">Давальческий материал</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr class="table-custom-header">
<th>Материал</th>
<th class="text-center" style="width:140px;">Есть на складе</th>
<th class="text-center" style="width:180px;">К отгрузке</th>
</tr>
</thead>
<tbody>
{% for r in material_rows %}
<tr>
<td class="fw-bold">{{ r.material.full_name|default:r.material.name }}</td>
<td class="text-center">{{ r.available }}</td>
<td class="text-center">
<input
class="form-control bg-body text-body border-secondary ship-qty"
type="number"
min="0"
step="0.001"
name="mat_{{ r.material.id }}"
value="0"
data-label="{{ r.material.full_name|default:r.material.name }}"
>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="d-flex justify-content-end mt-3">
{% if can_edit %}
<button type="button" class="btn btn-outline-accent" data-bs-toggle="modal" data-bs-target="#shipConfirmModal" id="shipOpenConfirm">
Отгрузить
</button>
{% else %}
<button type="button" class="btn btn-outline-secondary" disabled>Отгрузить</button>
{% endif %}
</div>
<div class="modal fade" id="shipConfirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<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">
<div class="small text-muted mb-2">Проверь итоговый список к отгрузке:</div>
<div id="shipSummary" class="border border-secondary rounded p-2"></div>
<div id="shipSummaryEmpty" class="text-muted d-none">Нечего отгружать (везде 0).</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" id="shipConfirmBtn">Принять отгрузку</button>
</div>
</div>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const openBtn = document.getElementById('shipOpenConfirm');
const confirmBtn = document.getElementById('shipConfirmBtn');
const summary = document.getElementById('shipSummary');
const empty = document.getElementById('shipSummaryEmpty');
const inputs = Array.from(document.querySelectorAll('#shipForm .ship-qty'));
function buildSummary() {
const rows = [];
inputs.forEach(inp => {
const raw = (inp.value || '').toString().trim();
if (!raw) return;
const val = parseFloat(raw.replace(',', '.'));
if (!val || val <= 0) return;
const label = inp.getAttribute('data-label') || '';
rows.push({ label, val });
});
if (!rows.length) {
summary.innerHTML = '';
empty.classList.remove('d-none');
if (confirmBtn) confirmBtn.disabled = true;
return;
}
empty.classList.add('d-none');
if (confirmBtn) confirmBtn.disabled = false;
summary.innerHTML = rows.map(r => {
return `<div class="d-flex justify-content-between gap-2"><div>${r.label}</div><div class="fw-bold">${r.val}</div></div>`;
}).join('');
}
openBtn?.addEventListener('click', () => buildSummary());
});
</script>
{% else %}
<div class="text-muted mt-3">Выбери сделку, чтобы сформировать список к отгрузке.</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -169,20 +169,7 @@
<i class="bi bi-arrow-left-right me-1"></i>Переместить
</button>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal"
data-bs-target="#transferModal"
data-mode="ship"
data-stock-item-id="{{ it.id }}"
data-stock-item-name="{% if it.material_id %}{{ it.material.full_name }}{% elif it.entity_id %}{{ it.entity }}{% else %}—{% endif %}"
data-from-location="{{ it.location }}"
data-from-location-id="{{ it.location_id }}"
data-max="{{ it.quantity }}"
>
<i class="bi bi-truck me-1"></i>Отгрузка
</button>
</div>
{% else %}
<span class="text-muted small">только просмотр</span>