Конкретно пересмотрел логику работы. Легаси вынесена в архив
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s

This commit is contained in:
2026-04-13 07:36:57 +03:00
parent 86215c9fa8
commit 28537447f8
80 changed files with 10246 additions and 684 deletions

View File

@@ -0,0 +1,141 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm border-secondary mb-4">
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
<h3 class="text-accent mb-0">
<i class="bi bi-check2-square me-2"></i>Закрытие сборки
</h3>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'workitem_detail' workitem.id %}">Назад к заданию</a>
</div>
<div class="card-body p-4">
<div class="mb-4">
<h5 class="fw-bold">{{ workitem.entity.drawing_number|default:"—" }} {{ workitem.entity.name }}</h5>
<div class="text-muted small">Сделка № {{ workitem.deal.number }}</div>
<div class="text-muted small">План: {{ workitem.quantity_plan }} шт. · Собрано: {{ workitem.quantity_done }} шт.</div>
<div class="text-muted small">Осталось собрать: <strong>{{ remaining }}</strong> шт.</div>
{% if to_location %}
<div class="text-muted small mt-2">Участок сборки (склад): <strong>{{ to_location.name }}</strong></div>
{% else %}
<div class="text-danger small mt-2 fw-bold">Участок сборки не определен! Закрытие невозможно.</div>
{% endif %}
<div class="text-muted small mt-2">
Пост для отчёта:
{% if workitem.machine_id %}
<strong>{{ workitem.machine.name }}</strong>
{% else %}
<strong class="text-warning">не выбран</strong>
{% endif %}
</div>
</div>
{% if error %}
<div class="alert alert-warning border-warning">
{{ error }}
</div>
{% else %}
<h6 class="fw-bold border-bottom border-secondary pb-2 mb-3">Наличие компонентов на участке</h6>
<div class="table-responsive mb-4">
<table class="table table-sm table-hover align-middle">
<thead class="table-custom-header">
<tr>
<th>Компонент</th>
<th class="text-center">Нужно на 1 шт</th>
<th class="text-center">Есть на участке</th>
<th class="text-center">Хватит на сборок</th>
</tr>
</thead>
<tbody>
{% for c in components %}
<tr>
<td>
<div class="fw-bold">{{ c.entity.drawing_number|default:"—" }} {{ c.entity.name }}</div>
<div class="small text-muted">{{ c.entity.get_entity_type_display }}</div>
</td>
<td class="text-center">{{ c.req_per_1 }}</td>
<td class="text-center">{{ c.available|floatformat:2 }}</td>
<td class="text-center fw-bold {% if c.max_possible == 0 %}text-danger{% else %}text-success{% endif %}">
{{ c.max_possible }}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center text-muted">Спецификация пуста или не найдена.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="alert alert-info border-info d-flex justify-content-between align-items-center">
<div>
<strong>Максимум можно закрыть сейчас:</strong> {{ max_possible }} шт.
</div>
</div>
<form method="post" action="">
{% csrf_token %}
<input type="hidden" name="action" value="close">
<div class="row align-items-end g-2">
<div class="col-md-6">
<label class="form-label text-muted small mb-1">Фактически собрано (шт.)</label>
<input type="number" class="form-control border-secondary" name="fact_qty" min="1" max="{{ max_possible }}" value="{{ max_possible }}" {% if max_possible == 0 %}disabled{% endif %}>
</div>
<div class="col-md-6">
{% if workitem.machine_id %}
<button type="submit" class="btn btn-warning w-100" {% if max_possible == 0 %}disabled{% endif %}>
Списать компоненты и закрыть сборку
</button>
{% else %}
<button type="button" class="btn btn-warning w-100" data-bs-toggle="modal" data-bs-target="#selectMachineModal" {% if max_possible == 0 %}disabled{% endif %}>
Выбрать пост и закрыть
</button>
{% endif %}
</div>
</div>
<div class="small text-muted mt-2">
При закрытии компоненты будут списаны со склада участка <strong>{{ to_location.name }}</strong>, а готовая сборка будет оприходована на этот же участок. Производственный отчёт привязывается к выбранному посту.
</div>
<div class="modal fade" id="selectMachineModal" 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="Close"></button>
</div>
<div class="modal-body">
{% if workshop_machines %}
<label class="form-label small text-muted mb-1">Пост</label>
<select class="form-select border-secondary" name="machine_id" required>
<option value="">— выбрать —</option>
{% for m in workshop_machines %}
<option value="{{ m.id }}">{{ m.name }}</option>
{% endfor %}
</select>
{% else %}
<div class="alert alert-warning border-warning mb-0">
В этом цехе нет постов. Создай пост в «Справочники → Производство → Посты/станки» и привяжи его к этому цеху.
</div>
{% endif %}
</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-warning" {% if not workshop_machines %}disabled{% endif %}>Закрыть</button>
</div>
</div>
</div>
</div>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -48,27 +48,27 @@
<th>Дата</th>
<th>Сделка</th>
<th>Деталь</th>
<th>План</th>
<th>К закрытию</th>
<th data-sort="false">Факт</th>
<th data-sort="false">Режим</th>
</tr>
</thead>
<tbody>
{% for it in items %}
{% for wi in workitems %}
<tr>
<td class="small">{{ it.date|date:"d.m.Y" }}</td>
<td><span class="text-accent fw-bold">{{ it.task.deal.number }}</span></td>
<td class="fw-bold">{{ it.task.drawing_name }}</td>
<td>{{ it.quantity_plan }}</td>
<td class="small">{{ wi.date|date:"d.m.Y" }}</td>
<td><span class="text-accent fw-bold">{{ wi.deal.number }}</span></td>
<td class="fw-bold">{{ wi.entity.drawing_number|default:"—" }} {{ wi.entity.name }}</td>
<td>{{ wi.remaining }}</td>
<td style="max-width:140px;">
<input class="form-control form-control-sm border-secondary" type="number" min="0" max="{{ it.quantity_plan }}" name="fact_{{ it.id }}" id="fact_{{ it.id }}" value="{{ it.quantity_fact }}" {% if not can_edit %}disabled{% endif %}>
<input class="form-control form-control-sm border-secondary" type="number" min="0" max="{{ wi.remaining }}" name="fact_{{ wi.id }}" id="fact_{{ wi.id }}" value="0" {% if not can_edit %}disabled{% endif %}>
</td>
<td style="min-width:260px;">
<div class="d-flex gap-2 align-items-center flex-wrap">
<button type="button" class="btn btn-sm btn-outline-success closing-set-action" data-item-id="{{ it.id }}" data-action="done" data-plan="{{ it.quantity_plan }}" {% if not can_edit %}disabled{% endif %}>Полностью</button>
<button type="button" class="btn btn-sm btn-outline-warning closing-set-action" data-item-id="{{ it.id }}" data-action="partial" data-plan="{{ it.quantity_plan }}" {% if not can_edit %}disabled{% endif %}>Частично</button>
<input type="hidden" id="ca_{{ it.id }}" name="close_action_{{ it.id }}" value="">
<span class="small text-muted" id="modeLabel_{{ it.id }}"></span>
<button type="button" class="btn btn-sm btn-outline-success closing-set-action" data-item-id="{{ wi.id }}" data-action="done" data-plan="{{ wi.remaining }}" {% if not can_edit %}disabled{% endif %}>Полностью</button>
<button type="button" class="btn btn-sm btn-outline-warning closing-set-action" data-item-id="{{ wi.id }}" data-action="partial" data-plan="{{ wi.remaining }}" {% if not can_edit %}disabled{% endif %}>Частично</button>
<input type="hidden" id="ca_{{ wi.id }}" name="close_action_{{ wi.id }}" value="">
<span class="small text-muted" id="modeLabel_{{ wi.id }}"></span>
</div>
</td>
</tr>

View File

@@ -0,0 +1,78 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<h3 class="text-accent mb-0">
<i class="bi bi-check2-square me-2"></i>Закрытие · Мои сменные задания
</h3>
<form class="d-flex flex-wrap gap-2 align-items-end" method="get">
<div>
<label class="form-label small text-muted mb-1">Поиск</label>
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="Сделка / КД / станок / цех">
</div>
<button class="btn btn-outline-accent btn-sm" type="submit">
<i class="bi bi-search me-1"></i>Показать
</button>
<a class="btn btn-outline-accent btn-sm" href="{% url 'closing_workitems' %}">
<i class="bi bi-arrow-counterclockwise me-1"></i>Сброс
</a>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:110px;">Сделка</th>
<th>КД</th>
<th class="text-center" style="width:180px;">Операция</th>
<th class="text-center" style="width:160px;">Цех/Пост</th>
<th class="text-center" style="width:90px;">План</th>
<th class="text-center" style="width:90px;">Факт</th>
<th class="text-center" style="width:110px;">Остаток</th>
<th class="text-center" style="width:120px;">Действие</th>
</tr>
</thead>
<tbody>
{% for wi in workitems %}
<tr>
<td class="fw-bold">
<a class="text-decoration-none" href="{% url 'planning_deal' wi.deal.id %}">{{ wi.deal.number }}</a>
</td>
<td>
<div class="fw-bold">
<a class="text-decoration-none text-reset" href="{% url 'workitem_detail' wi.id %}">
{{ wi.entity.drawing_number|default:"—" }} {{ wi.entity.name }}
</a>
</div>
<div class="small text-muted">{{ wi.entity.get_entity_type_display }}</div>
</td>
<td class="text-center">
{% if wi.operation %}{{ wi.operation.name }}{% else %}{{ wi.stage|default:"—" }}{% endif %}
</td>
<td class="text-center">
{% if wi.machine %}{{ wi.machine.name }}{% elif wi.workshop %}{{ wi.workshop.name }}{% else %}—{% endif %}
</td>
<td class="text-center">{{ wi.quantity_plan }}</td>
<td class="text-center">{{ wi.quantity_done }}</td>
<td class="text-center fw-bold {% if wi.remaining > 0 %}text-warning{% else %}text-success{% endif %}">
{{ wi.remaining }}
</td>
<td class="text-center">
<a class="btn btn-outline-warning btn-sm" href="{{ wi.close_url }}">
Закрыть
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center text-muted py-4">Нет активных сменных заданий.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -86,6 +86,10 @@
<option value="done">Завершена</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Срок отгрузки</label>
<input type="date" class="form-control border-secondary" id="dealDueDate">
</div>
<div class="mb-0">
<label class="form-label small text-muted">Описание</label>
<textarea class="form-control border-secondary" rows="3" id="dealDescription"></textarea>
@@ -112,6 +116,7 @@ document.addEventListener('DOMContentLoaded', function () {
const dealNumber = document.getElementById('dealNumber');
const dealStatus = document.getElementById('dealStatus');
const dealDescription = document.getElementById('dealDescription');
const dealDueDate = document.getElementById('dealDueDate');
const dealSaveBtn = document.getElementById('dealSaveBtn');
function getCookie(name) {
@@ -139,6 +144,7 @@ document.addEventListener('DOMContentLoaded', function () {
dealModal.addEventListener('show.bs.modal', function () {
if (dealNumber) dealNumber.value = '';
if (dealDescription) dealDescription.value = '';
if (dealDueDate) dealDueDate.value = '';
if (dealStatus) dealStatus.value = 'work';
});
}
@@ -150,6 +156,7 @@ document.addEventListener('DOMContentLoaded', function () {
status: dealStatus ? dealStatus.value : 'work',
company_id: '{{ company.id }}',
description: (dealDescription ? dealDescription.value : ''),
due_date: dealDueDate ? dealDueDate.value : '',
};
await postForm('{% url "deal_upsert" %}', payload);
window.location.reload();

View File

@@ -0,0 +1,36 @@
{% extends 'base.html' %}
{% 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-journals me-2"></i>Справочники</h3>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-outline-accent" href="{% url 'products' %}">
<i class="bi bi-diagram-3 me-2"></i>Номенклатура изделий
</a>
<a class="btn btn-outline-accent" href="{% url 'supply_catalog' %}">
<i class="bi bi-box-seam me-2"></i>Номенклатура снабжения (покупное/аутсорс)
</a>
</div>
<div class="mt-3">
<div class="btn-group" role="group" aria-label="Материалы">
<a class="btn btn-outline-accent" href="{% url 'materials_catalog' %}">Материалы</a>
<a class="btn btn-outline-accent" href="{% url 'material_categories_catalog' %}">Категории материалов</a>
<a class="btn btn-outline-accent" href="{% url 'steel_grades_catalog' %}">Марки стали</a>
</div>
</div>
<div class="mt-3">
<div class="btn-group" role="group" aria-label="Производство">
<a class="btn btn-outline-accent" href="{% url 'locations_catalog' %}">Склады</a>
<a class="btn btn-outline-accent" href="{% url 'workshops_catalog' %}">Цеха</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,291 @@
{% extends 'base.html' %}
{% block content %}
<div class="card border-secondary mb-3 shadow-sm">
<div class="card-body py-2">
<form method="get" class="row g-2 align-items-center">
<div class="col-md-4">
<label class="small text-muted mb-1 fw-bold">Станок:</label>
<select class="form-select form-select-sm bg-body text-body border-secondary" name="machine_id" onchange="this.form.submit()">
<option value="">— выбрать —</option>
{% for m in machines %}
<option value="{{ m.id }}" {% if selected_machine_id == m.id|stringformat:"s" %}selected{% endif %}>{{ m.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="small text-muted mb-1 fw-bold">Материал:</label>
<select class="form-select form-select-sm bg-body text-body border-secondary" name="material_id" onchange="this.form.submit()">
<option value="">— выбрать —</option>
{% for mat in materials %}
<option value="{{ mat.id }}" {% if selected_material_id == mat.id|stringformat:"s" %}selected{% endif %}>{{ mat.full_name|default:mat.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 text-end mt-auto">
<a class="btn btn-outline-secondary btn-sm w-100" href="{% url 'legacy_closing' %}">Сброс</a>
</div>
</form>
</div>
</div>
<form method="post">
{% csrf_token %}
<input type="hidden" name="machine_id" value="{{ selected_machine_id }}">
<input type="hidden" name="material_id" value="{{ selected_material_id }}">
<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-0"><i class="bi bi-archive me-2"></i>Архив / Закрытие</h3>
<div class="small text-muted">Legacy: Item</div>
</div>
</div>
<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>План</th>
<th data-sort="false">Факт</th>
<th data-sort="false">Режим</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<tr>
<td class="small">{{ it.date|date:"d.m.Y" }}</td>
<td><span class="text-accent fw-bold">{{ it.task.deal.number }}</span></td>
<td class="fw-bold">{{ it.task.drawing_name }}</td>
<td>{{ it.quantity_plan }}</td>
<td style="max-width:140px;">
<input class="form-control form-control-sm border-secondary" type="number" min="0" max="{{ it.quantity_plan }}" name="fact_{{ it.id }}" id="fact_{{ it.id }}" value="{{ it.quantity_fact }}" {% if not can_edit %}disabled{% endif %}>
</td>
<td style="min-width:260px;">
<div class="d-flex gap-2 align-items-center flex-wrap">
<button type="button" class="btn btn-sm btn-outline-success closing-set-action" data-item-id="{{ it.id }}" data-action="done" data-plan="{{ it.quantity_plan }}" {% if not can_edit %}disabled{% endif %}>Полностью</button>
<button type="button" class="btn btn-sm btn-outline-warning closing-set-action" data-item-id="{{ it.id }}" data-action="partial" data-plan="{{ it.quantity_plan }}" {% if not can_edit %}disabled{% endif %}>Частично</button>
<input type="hidden" id="ca_{{ it.id }}" name="close_action_{{ it.id }}" value="">
<span class="small text-muted" id="modeLabel_{{ it.id }}"></span>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="6" class="text-center text-muted py-4">Выбери станок и материал</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3">
<h5 class="mb-0">Списание со склада цеха (единицы)</h5>
</div>
<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>Размеры</th>
<th>Доступно</th>
<th data-sort="false">Использовано</th>
</tr>
</thead>
<tbody>
{% for s in stock_items %}
<tr>
<td class="small">{% if s.created_at %}{{ s.created_at|date:"d.m.Y H:i" }}{% endif %}</td>
<td>
{% if s.deal_id %}
<span class="text-accent fw-bold">{{ s.deal.number }}</span>
{% else %}
{% endif %}
</td>
<td>{{ s }}</td>
<td>
{% if s.current_length and s.current_width %}
{{ s.current_length|floatformat:"-g" }} × {{ s.current_width|floatformat:"-g" }} мм
{% elif s.current_length %}
{{ s.current_length|floatformat:"-g" }} мм
{% else %}
{% endif %}
</td>
<td>{{ s.quantity }}</td>
<td style="max-width:140px;">
<input class="form-control form-control-sm border-secondary" name="consume_{{ s.id }}" placeholder="0" {% if not can_edit %}disabled{% endif %}>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Нет единиц на складе для выбранного материала</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0">Остаток ДО</h5>
<button type="button" class="btn btn-outline-accent btn-sm" id="addRemnantBtn" {% if not can_edit %}disabled{% endif %}>Добавить ДО</button>
</div>
<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 data-sort="false"></th>
</tr>
</thead>
<tbody id="remnantBody">
<tr id="remnantEmptyRow">
<td colspan="4" class="text-center text-muted py-4">ДО не добавлены</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-outline-accent" {% if not can_edit %}disabled{% endif %}>Сохранить</button>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', () => {
const canEdit = {% if can_edit %}true{% else %}false{% endif %};
document.querySelectorAll('.closing-set-action').forEach(btn => {
btn.addEventListener('click', () => {
if (!canEdit) return;
const itemId = btn.getAttribute('data-item-id');
const action = btn.getAttribute('data-action');
const plan = parseInt(btn.getAttribute('data-plan') || '0', 10) || 0;
const hidden = document.getElementById('ca_' + itemId);
const fact = document.getElementById('fact_' + itemId);
const label = document.getElementById('modeLabel_' + itemId);
if (hidden) hidden.value = action;
const cell = btn.closest('td');
if (cell) {
cell.querySelectorAll('.closing-set-action').forEach(b => {
const a = b.getAttribute('data-action');
if (a === 'done') {
b.classList.remove('btn-success');
b.classList.add('btn-outline-success');
}
if (a === 'partial') {
b.classList.remove('btn-warning');
b.classList.add('btn-outline-warning');
}
});
}
if (action === 'done') {
btn.classList.remove('btn-outline-success');
btn.classList.add('btn-success');
if (fact) {
fact.value = String(plan);
fact.readOnly = true;
}
if (label) label.textContent = 'Выбрано: полностью';
}
if (action === 'partial') {
btn.classList.remove('btn-outline-warning');
btn.classList.add('btn-warning');
if (fact) {
fact.readOnly = false;
fact.focus();
fact.select();
}
if (label) label.textContent = 'Выбрано: частично';
}
});
});
const addBtn = document.getElementById('addRemnantBtn');
const body = document.getElementById('remnantBody');
const emptyRow = document.getElementById('remnantEmptyRow');
function renumberRemnants() {
const rows = Array.from(body.querySelectorAll('tr[data-remnant-row="1"]'));
rows.forEach((tr, idx) => {
const qty = tr.querySelector('input[data-field="qty"]');
const len = tr.querySelector('input[data-field="len"]');
const wid = tr.querySelector('input[data-field="wid"]');
if (qty) qty.name = 'remnant_qty_' + idx;
if (len) len.name = 'remnant_len_' + idx;
if (wid) wid.name = 'remnant_wid_' + idx;
});
if (emptyRow) {
emptyRow.style.display = rows.length ? 'none' : '';
}
}
function addRemnantRow() {
if (!canEdit) return;
const rows = Array.from(body.querySelectorAll('tr[data-remnant-row="1"]'));
if (rows.length >= 50) return;
const tr = document.createElement('tr');
tr.setAttribute('data-remnant-row', '1');
tr.innerHTML = `
<td style="max-width:180px;">
<input class="form-control form-control-sm border-secondary" data-field="qty" inputmode="decimal" placeholder="Кол-во" required>
</td>
<td style="max-width:180px;">
<input class="form-control form-control-sm border-secondary" data-field="len" inputmode="decimal" placeholder="Длина (мм)">
</td>
<td style="max-width:180px;">
<input class="form-control form-control-sm border-secondary" data-field="wid" inputmode="decimal" placeholder="Ширина (мм)">
</td>
<td class="text-end">
<button type="button" class="btn btn-outline-secondary btn-sm" data-action="remove">Удалить</button>
</td>
`;
const rm = tr.querySelector('button[data-action="remove"]');
if (rm) {
rm.addEventListener('click', () => {
tr.remove();
renumberRemnants();
});
}
body.appendChild(tr);
renumberRemnants();
const first = tr.querySelector('input[data-field="qty"]');
if (first) {
first.focus();
first.select();
}
}
if (addBtn) {
addBtn.addEventListener('click', addRemnantRow);
}
renumberRemnants();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block content %}
{% include 'shiftflow/partials/_filter.html' %}
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<div>
<h3 class="text-accent mb-0"><i class="bi bi-archive me-2"></i>Архив / Реестр</h3>
<div class="small text-muted">Legacy: Item</div>
</div>
{% if user_role in 'admin,technologist,master' %}
<a class="btn btn-outline-secondary btn-sm" target="_blank" href="{% url 'registry_print' %}?{{ request.GET.urlencode }}">
<i class="bi bi-printer me-1"></i>Печать
</a>
{% endif %}
</div>
{% include 'shiftflow/partials/_items_table.html' with items=items %}
</div>
{% endblock %}

View File

@@ -0,0 +1,173 @@
{% extends 'base.html' %}
{% block content %}
<div class="card border-secondary mb-3 shadow-sm">
<div class="card-body py-2">
<form method="get" class="row g-2 align-items-end">
<div class="col-md-auto">
<label class="small text-muted mb-1 fw-bold">Период (с):</label>
<input type="date" name="start_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ start_date }}">
</div>
<div class="col-md-auto">
<label class="small text-muted mb-1 fw-bold">Период (по):</label>
<input type="date" name="end_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ end_date }}">
</div>
<div class="col-md-auto">
<button type="submit" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-funnel me-1"></i>Показать
</button>
</div>
<div class="col-md-auto">
<a href="{% url 'legacy_writeoffs' %}?reset=1" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-counterclockwise me-1"></i>Сброс
</a>
</div>
</form>
</div>
</div>
<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-0"><i class="bi bi-archive me-2"></i>Архив / Списание / Производство</h3>
<div class="small text-muted">По производственным отчетам</div>
</div>
</div>
<div class="card-body">
{% for card in report_cards %}
<div class="border border-secondary rounded p-3 mb-3">
<div class="d-flex flex-wrap justify-content-between gap-2">
<div class="fw-bold">
{{ card.report.date|date:"d.m.Y" }} — {{ card.report.machine }} — {{ card.report.operator }}
<span class="text-muted small ms-2">#{{ card.report.id }}</span>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-lg-4">
<div class="small text-muted fw-bold mb-1">Списано</div>
{% if card.report.consumptions.all %}
<ul class="mb-0">
{% for c in card.report.consumptions.all %}
{% if c.stock_item_id and c.stock_item.material_id %}
<li>
{{ c.stock_item.material.full_name|default:c.stock_item.material.name }}
({% if c.stock_item.current_length and c.stock_item.current_width %}{{ c.stock_item.current_length|floatformat:"-g" }}×{{ c.stock_item.current_width|floatformat:"-g" }}{% elif c.stock_item.current_length %}{{ c.stock_item.current_length|floatformat:"-g" }}{% else %}—{% endif %})
{{ c.quantity|floatformat:"-g" }} шт
</li>
{% elif c.material_id %}
<li>{{ c.material }} {{ c.quantity|floatformat:"-g" }} шт</li>
{% else %}
<li>— {{ c.quantity|floatformat:"-g" }} шт</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
<div class="text-muted small"></div>
{% endif %}
</div>
<div class="col-lg-4">
<div class="small text-muted fw-bold mb-1">Произведено</div>
{% if card.produced %}
<ul class="mb-0">
{% for k,v in card.produced.items %}
<li>{{ k }}: {{ v }} шт</li>
{% endfor %}
</ul>
{% else %}
<div class="text-muted small"></div>
{% endif %}
</div>
<div class="col-lg-4">
<div class="small text-muted fw-bold mb-1">Остаток ДО</div>
{% if card.report.remnants.all %}
<ul class="mb-0">
{% for r in card.report.remnants.all %}
<li>
{{ r.material.full_name|default:r.material.name|default:r.material }}
({% if r.current_length and r.current_width %}{{ r.current_length|floatformat:"-g" }}×{{ r.current_width|floatformat:"-g" }}{% elif r.current_length %}{{ r.current_length|floatformat:"-g" }}{% else %}—{% endif %})
{{ r.quantity|floatformat:"-g" }} шт
</li>
{% endfor %}
</ul>
{% else %}
<div class="text-muted small"></div>
{% endif %}
</div>
</div>
</div>
{% empty %}
<div class="text-muted">За выбранный период отчётов нет.</div>
{% endfor %}
</div>
</div>
<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-check2-square me-2"></i>Сменные задания (1С)</h3>
<div class="small text-muted">Отметка «Списано в 1С»</div>
</div>
<form method="post" class="mb-0">
{% csrf_token %}
<input type="hidden" name="start_date" value="{{ start_date }}">
<input type="hidden" name="end_date" value="{{ end_date }}">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th data-sort="false"></th>
<th>Дата</th>
<th>Сделка</th>
<th>Станок</th>
<th>Позиция</th>
<th>План / Факт</th>
<th data-sort="false" class="text-center">1С</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<tr>
<td style="width:40px;">
{% if can_edit %}
<input type="checkbox" class="form-check-input" name="item_ids" value="{{ it.id }}">
{% endif %}
</td>
<td class="small">{{ it.date|date:"d.m.Y" }}</td>
<td><span class="text-accent fw-bold">{{ it.task.deal.number|default:"-" }}</span></td>
<td><span class="badge bg-dark border border-secondary">{{ it.machine.name }}</span></td>
<td class="fw-bold">{{ it.task.drawing_name|default:"—" }}</td>
<td>
<span class="text-info fw-bold">{{ it.quantity_plan }}</span> /
<span class="text-success">{{ it.quantity_fact }}</span>
</td>
<td class="text-center">
{% if it.is_synced_1c %}
<i class="bi bi-check-circle-fill text-success" title="Учтено"></i>
{% else %}
<i class="bi bi-clock-history text-muted" title="Ожидает"></i>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="7" class="text-center text-muted py-4">Нет сменных заданий за период</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if can_edit %}
<div class="card-body border-top border-secondary d-flex justify-content-end">
<button type="submit" class="btn btn-outline-accent">
<i class="bi bi-save me-2"></i>Сохранить
</button>
</div>
{% endif %}
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,72 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<h3 class="text-accent mb-0"><i class="bi bi-boxes me-2"></i>Справочник · Склады</h3>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'directories' %}">Назад</a>
</div>
{% if can_edit %}
<div class="card-body border-bottom border-secondary">
<form method="post" class="row g-2 align-items-end">
{% csrf_token %}
<input type="hidden" name="action" value="create">
<div class="col-md-7">
<label class="form-label small text-muted mb-1">Название склада</label>
<input class="form-control border-secondary" name="name" placeholder="Напр: Центральный склад" required>
</div>
<div class="col-md-2">
<button class="btn btn-outline-accent w-100" type="submit">Создать</button>
</div>
</form>
</div>
{% endif %}
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th class="text-center" style="width:80px;" data-sort-type="number"></th>
<th>Склад</th>
<th class="text-center" style="width:140px;">Действия</th>
</tr>
</thead>
<tbody>
{% for l in locations %}
<tr>
<td class="text-center">{{ forloop.counter }}</td>
<td class="fw-bold">
{% if can_edit %}
<form method="post" class="row g-2 align-items-center">
{% csrf_token %}
<input type="hidden" name="action" value="update">
<input type="hidden" name="location_id" value="{{ l.id }}">
<div class="col-12">
<input class="form-control form-control-sm border-secondary" name="name" value="{{ l.name }}">
</div>
{% else %}
{{ l.name }}
{% endif %}
</td>
<td class="text-center">
{% if can_edit %}
<button class="btn btn-outline-accent btn-sm" type="submit">Сохранить</button>
</form>
{% else %}
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center text-muted py-4">Складов нет.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,158 @@
{% 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-0"><i class="bi bi-building me-2"></i>{{ workshop.name }}</h3>
<div class="small text-muted">Цех · ID {{ workshop.id }}</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary btn-sm" href="{% url 'workshops_catalog' %}">Назад</a>
{% if can_edit %}
<button class="btn btn-outline-accent btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#addMachineModal">
<i class="bi bi-plus-circle me-1"></i>Добавить пост
</button>
{% endif %}
</div>
</div>
<div class="card-body border-bottom border-secondary">
{% if can_edit %}
<form method="post" class="row g-2 align-items-end">
{% csrf_token %}
<input type="hidden" name="action" value="update_workshop">
<div class="col-md-6">
<label class="form-label small text-muted mb-1">Наименование цеха</label>
<input class="form-control border-secondary" name="name" value="{{ workshop.name }}">
</div>
<div class="col-md-4">
<label class="form-label small text-muted mb-1">Склад цеха</label>
<select class="form-select border-secondary" name="location_id">
<option value="">— не задан —</option>
{% for l in locations %}
<option value="{{ l.id }}" {% if workshop.location_id == l.id %}selected{% endif %}>{{ l.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button class="btn btn-outline-accent w-100" type="submit">Сохранить</button>
</div>
</form>
{% else %}
<div class="row g-2">
<div class="col-md-6">
<div class="small text-muted">Цех</div>
<div class="fw-bold">{{ workshop.name }}</div>
</div>
<div class="col-md-6">
<div class="small text-muted">Склад цеха</div>
<div class="fw-bold">{{ workshop.location.name|default:"—" }}</div>
</div>
</div>
{% endif %}
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th class="text-center" style="width:80px;" data-sort-type="number"></th>
<th class="text-center" style="width:90px;" data-sort-type="number">ID</th>
<th>Пост/станок</th>
<th class="text-center" style="width:200px;">Тип</th>
<th class="text-center" style="width:220px;">Действия</th>
</tr>
</thead>
<tbody>
{% for m in machines %}
<tr>
<td class="text-center">{{ forloop.counter }}</td>
<td class="text-center text-muted">{{ m.id }}</td>
<td class="fw-bold">
{% if can_edit %}
<form method="post" class="row g-2 align-items-center">
{% csrf_token %}
<input type="hidden" name="action" value="update_machine">
<input type="hidden" name="machine_id" value="{{ m.id }}">
<div class="col-12">
<input class="form-control form-control-sm border-secondary" name="name" value="{{ m.name }}">
</div>
{% else %}
{{ m.name }}
{% endif %}
</td>
<td class="text-center">
{% if can_edit %}
<select class="form-select form-select-sm border-secondary" name="machine_type">
{% for k, v in machine_types %}
<option value="{{ k }}" {% if m.machine_type == k %}selected{% endif %}>{{ v }}</option>
{% endfor %}
</select>
{% else %}
{{ m.get_machine_type_display }}
{% endif %}
</td>
<td class="text-center">
{% if can_edit %}
<div class="d-flex justify-content-center gap-2">
<button class="btn btn-outline-accent btn-sm" type="submit">Сохранить</button>
</form>
<form method="post" class="m-0">
{% csrf_token %}
<input type="hidden" name="action" value="delete_machine">
<input type="hidden" name="machine_id" value="{{ m.id }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
</form>
</div>
{% else %}
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Постов нет.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if can_edit %}
<div class="modal fade" id="addMachineModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="create_machine">
<div class="modal-header border-secondary">
<h5 class="modal-title">Добавить пост</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<label class="form-label small text-muted mb-1">Название</label>
<input class="form-control border-secondary mb-3" name="name" placeholder="Напр: Сварка-1" required>
<label class="form-label small text-muted mb-1">Тип</label>
<select class="form-select border-secondary" name="machine_type">
{% for k, v in machine_types %}
<option value="{{ k }}">{{ v }}</option>
{% endfor %}
</select>
</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>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,149 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<nav aria-label="breadcrumb" class="small">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a class="text-decoration-none" href="{% url 'directories' %}">Справочники</a></li>
<li class="breadcrumb-item active" aria-current="page">Категории материалов</li>
</ol>
</nav>
<h3 class="text-accent mb-0"><i class="bi bi-tags me-2"></i>Категории материалов</h3>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<form method="get" class="d-flex gap-2 align-items-center">
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="Поиск...">
<button class="btn btn-outline-accent btn-sm" type="submit"><i class="bi bi-search me-1"></i>Найти</button>
<a class="btn btn-outline-accent btn-sm" href="{% url 'material_categories_catalog' %}"><i class="bi bi-arrow-counterclockwise me-1"></i>Сброс</a>
</form>
<a class="btn btn-outline-accent btn-sm" href="{% url 'directories' %}"><i class="bi bi-arrow-left me-1"></i>Назад</a>
{% if can_edit %}
<button class="btn btn-outline-accent btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#catModal" onclick="openCatCreate()">
<i class="bi bi-plus-lg me-1"></i>Создать
</button>
{% endif %}
</div>
</div>
<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>
</tr>
</thead>
<tbody>
{% for c in categories %}
<tr role="button" {% if can_edit %}onclick="openCatEdit(this)"{% endif %}
data-id="{{ c.id }}" data-name="{{ c.name }}" data-gost="{{ c.gost_standard }}" data-form="{{ c.form_factor }}">
<td class="fw-bold">{{ c.name }}</td>
<td>{{ c.gost_standard|default:"—" }}</td>
<td class="small text-muted">{{ c.get_form_factor_display }}</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center text-muted py-4">Нет данных</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="catModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form class="modal-content border-secondary" onsubmit="event.preventDefault(); saveCat();">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="catModalTitle">Категория</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="catId">
<div class="row g-2">
<div class="col-md-5">
<label class="form-label">Название</label>
<input class="form-control bg-body text-body border-secondary" id="catName" required {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<label class="form-label">ГОСТ</label>
<input class="form-control bg-body text-body border-secondary" id="catGost" {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-3">
<label class="form-label">Форма</label>
<select class="form-select bg-body text-body border-secondary" id="catForm" {% if not can_edit %}disabled{% endif %}>
<option value="sheet">Лист</option>
<option value="bar">Прокат/хлыст</option>
<option value="other">Прочее</option>
</select>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-accent" data-bs-dismiss="modal">Отмена</button>
{% if can_edit %}
<button type="submit" class="btn btn-outline-accent">Сохранить</button>
{% endif %}
</div>
</form>
</div>
</div>
<script>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
function openCatCreate() {
document.getElementById('catModalTitle').textContent = 'Категория (создание)';
document.getElementById('catId').value = '';
document.getElementById('catName').value = '';
document.getElementById('catGost').value = '';
document.getElementById('catForm').value = 'other';
new bootstrap.Modal(document.getElementById('catModal')).show();
}
function openCatEdit(tr) {
document.getElementById('catModalTitle').textContent = 'Категория (правка)';
document.getElementById('catId').value = tr.getAttribute('data-id') || '';
document.getElementById('catName').value = tr.getAttribute('data-name') || '';
document.getElementById('catGost').value = tr.getAttribute('data-gost') || '';
document.getElementById('catForm').value = tr.getAttribute('data-form') || 'other';
new bootstrap.Modal(document.getElementById('catModal')).show();
}
async function saveCat() {
const fd = new FormData();
fd.append('id', document.getElementById('catId').value);
fd.append('name', document.getElementById('catName').value);
fd.append('gost_standard', document.getElementById('catGost').value);
fd.append('form_factor', document.getElementById('catForm').value);
const res = await fetch("{% url 'material_category_upsert' %}", {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': getCookie('csrftoken') },
body: fd,
});
if (!res.ok) {
alert('Не удалось сохранить категорию');
return;
}
window.location.reload();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,179 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<nav aria-label="breadcrumb" class="small">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a class="text-decoration-none" href="{% url 'directories' %}">Справочники</a></li>
<li class="breadcrumb-item active" aria-current="page">Материалы</li>
</ol>
</nav>
<h3 class="text-accent mb-0"><i class="bi bi-box-seam me-2"></i>Материалы</h3>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<form method="get" class="d-flex gap-2 align-items-center">
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="Поиск...">
<button class="btn btn-outline-accent btn-sm" type="submit"><i class="bi bi-search me-1"></i>Найти</button>
<a class="btn btn-outline-accent btn-sm" href="{% url 'materials_catalog' %}"><i class="bi bi-arrow-counterclockwise me-1"></i>Сброс</a>
</form>
<a class="btn btn-outline-accent btn-sm" href="{% url 'directories' %}"><i class="bi bi-arrow-left me-1"></i>Назад</a>
{% if can_edit %}
<button class="btn btn-outline-accent btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#materialModal" onclick="openMaterialCreate()">
<i class="bi bi-plus-lg me-1"></i>Создать
</button>
{% endif %}
</div>
</div>
<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>Полное имя</th>
<th>Масса</th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr role="button" {% if can_edit %}onclick="openMaterialEdit({{ r.m.id }})"{% endif %}>
<td>{{ r.m.category.name }}</td>
<td>{{ r.m.steel_grade.name|default:"—" }}</td>
<td class="fw-bold">{{ r.m.name }}</td>
<td class="small text-muted">{{ r.m.full_name }}</td>
<td>
{% if r.m.mass_per_unit %}
{{ r.m.mass_per_unit|floatformat:"-g" }} {{ r.unit }}
{% else %}
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Нет данных</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="materialModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form class="modal-content border-secondary" onsubmit="event.preventDefault(); saveMaterial();">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="materialModalTitle">Материал</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="materialId">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Категория</label>
<select class="form-select bg-body text-body border-secondary" id="materialCategory" required {% if not can_edit %}disabled{% endif %}>
<option value="">— выбрать —</option>
{% for c in categories %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Марка стали</label>
<select class="form-select bg-body text-body border-secondary" id="materialGrade" {% if not can_edit %}disabled{% endif %}>
<option value="">— не выбрана —</option>
{% for g in grades %}
<option value="{{ g.id }}">{{ g.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-8">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" id="materialName" required {% if not can_edit %}disabled{% endif %}>
</div>
<div class="col-md-4">
<label class="form-label">Масса на ед. учёта</label>
<input class="form-control bg-body text-body border-secondary" id="materialMassPerUnit" inputmode="decimal" placeholder="Напр. 78.5" {% if not can_edit %}disabled{% endif %}>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-accent" data-bs-dismiss="modal">Отмена</button>
{% if can_edit %}
<button type="submit" class="btn btn-outline-accent">Сохранить</button>
{% endif %}
</div>
</form>
</div>
</div>
<script>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
function openMaterialCreate() {
document.getElementById('materialModalTitle').textContent = 'Материал (создание)';
document.getElementById('materialId').value = '';
document.getElementById('materialCategory').value = '';
document.getElementById('materialGrade').value = '';
document.getElementById('materialName').value = '';
document.getElementById('materialMassPerUnit').value = '';
new bootstrap.Modal(document.getElementById('materialModal')).show();
}
async function openMaterialEdit(id) {
const url = "{% url 'material_json' 1 %}".replace('/1/json/', `/${id}/json/`);
const res = await fetch(url, { credentials: 'same-origin' });
const data = await res.json();
document.getElementById('materialModalTitle').textContent = 'Материал (правка)';
document.getElementById('materialId').value = data.id;
document.getElementById('materialCategory').value = data.category_id || '';
document.getElementById('materialGrade').value = data.steel_grade_id || '';
document.getElementById('materialName').value = data.name || '';
document.getElementById('materialMassPerUnit').value = (data.mass_per_unit ?? '');
new bootstrap.Modal(document.getElementById('materialModal')).show();
}
async function saveMaterial() {
const fd = new FormData();
fd.append('id', document.getElementById('materialId').value);
fd.append('category_id', document.getElementById('materialCategory').value);
fd.append('steel_grade_id', document.getElementById('materialGrade').value);
fd.append('name', document.getElementById('materialName').value);
fd.append('mass_per_unit', document.getElementById('materialMassPerUnit').value);
const res = await fetch("{% url 'material_upsert' %}", {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': getCookie('csrftoken') },
body: fd,
});
if (!res.ok) {
alert('Не удалось сохранить материал');
return;
}
window.location.reload();
}
</script>
{% endblock %}

View File

@@ -25,6 +25,9 @@
<input type="checkbox" class="btn-check" name="statuses" id="s_work" value="work" {% if 'work' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_leftover" value="leftover" {% if 'leftover' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-secondary btn-sm" for="s_leftover">Недодел</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_closed" value="closed" {% if 'closed' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-success btn-sm" for="s_closed">Завершено</label>
{% else %}
@@ -32,29 +35,14 @@
<label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_leftover" value="leftover" {% if 'leftover' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-danger btn-sm" for="s_leftover">Недодел</label>
<label class="btn btn-outline-secondary btn-sm" for="s_leftover">Недодел</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_closed" value="closed" {% if 'closed' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-success btn-sm" for="s_closed">Завершено</label>
{% if user_role in 'admin,technologist' %}
<input type="checkbox" class="btn-check" name="statuses" id="s_imported" value="imported" {% if 'imported' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-accent btn-sm" for="s_imported">Импорт</label>
{% endif %}
{% endif %}
</div>
</div>
{% if user_role in 'admin,technologist,clerk' %}
<div class="col-md-auto">
<label class="small text-muted mb-1 fw-bold">Учёт 1С:</label>
<select name="is_synced" class="form-select form-select-sm bg-body text-body border-secondary registry-filter-1c" onchange="this.form.submit()">
<option value="" {% if not is_synced %}selected{% endif %}>Все</option>
<option value="1" {% if is_synced == '1' %}selected{% endif %}>Учтено</option>
<option value="0" {% if is_synced == '0' %}selected{% endif %}>Ожидает</option>
</select>
</div>
{% endif %}
<div class="col-md-auto">
<label class="small text-muted mb-1 fw-bold">С:</label>
@@ -95,8 +83,7 @@
const data = {
statuses: Array.from(form.querySelectorAll('input[name="statuses"]:checked')).map(i=>i.value),
m_ids: Array.from(form.querySelectorAll('input[name="m_ids"]:checked')).map(i=>i.value),
start_date: s ? s.value : '',
is_synced: (form.querySelector('select[name="is_synced"]')||{}).value || ''
start_date: s ? s.value : ''
};
try { localStorage.setItem('registry_filters', JSON.stringify(data)); } catch(_){}
}
@@ -122,8 +109,6 @@
}
if (s) s.value = data.start_date || weekAgo;
if (e) e.value = today;
const sel = form.querySelector('select[name="is_synced"]');
if (sel && data.is_synced !== undefined) sel.value = data.is_synced;
const filtered = form.querySelector('input[name="filtered"]');
if (filtered) filtered.value = '1';
form.submit();

View File

@@ -0,0 +1,93 @@
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th data-sort-type="date">Дата</th>
<th>Сделка</th>
<th>Цех/Пост</th>
<th>Наименование</th>
<th>Материал</th>
<th data-sort="false" class="text-center">Файлы</th>
<th data-sort="false" style="width: 160px;">Прогресс</th>
<th data-sort-type="number">План / Факт</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for wi in workitems %}
<tr class="workitem-row" data-href="{% url 'workitem_detail' wi.id %}">
<td class="small">{{ wi.date|date:"d.m.y" }}</td>
<td><span class="text-accent fw-bold">{{ wi.deal.number|default:"-" }}</span></td>
<td>
{% if wi.machine %}
<span class="badge bg-dark border border-secondary">{{ wi.workshop.name|default:"—" }}/{{ wi.machine.name }}</span>
{% elif wi.workshop %}
<span class="badge bg-dark border border-secondary">{{ wi.workshop.name }}</span>
{% else %}
<span class="badge bg-secondary"></span>
{% endif %}
</td>
<td class="fw-bold">
{{ wi.entity.drawing_number|default:"—" }} {{ wi.entity.name }}
</td>
<td class="small text-muted">
{% if wi.entity.planned_material %}
{{ wi.entity.planned_material.full_name|default:wi.entity.planned_material.name }}
{% else %}
{% endif %}
</td>
<td class="text-center">
{% if wi.entity.dxf_file %}
<a href="{{ wi.entity.dxf_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/IGES/STEP">
<i class="bi bi-file-earmark-code"></i>
</a>
{% endif %}
{% if wi.entity.pdf_main %}
<a href="{{ wi.entity.pdf_main.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертёж PDF">
<i class="bi bi-file-pdf"></i>
</a>
{% endif %}
</td>
<td>
<div class="progress bg-secondary-subtle border border-secondary sf-item-progress"
style="height: 10px;"
data-fact-width="{{ wi.fact_width|default:0 }}"
title="Факт: {{ wi.fact_pct|default:0 }}%">
<div class="progress-bar bg-warning sf-item-progress-bar"></div>
</div>
</td>
<td>
<span class="text-info fw-bold">{{ wi.quantity_plan }}</span> /
<span class="text-success">{{ wi.quantity_done }}</span>
</td>
<td>
{% if wi.status == 'done' %}
<span class="badge bg-success">{{ wi.get_status_display }}</span>
{% elif wi.status == 'leftover' %}
<span class="badge bg-secondary">{{ wi.get_status_display }}</span>
{% else %}
<span class="badge bg-primary">{{ wi.get_status_display }}</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="10" class="text-center p-5 text-muted">Записей WorkItem нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll(".workitem-row").forEach(row => {
row.addEventListener("click", function(e) {
if (e.target.closest('.stop-prop')) return;
const href = this.dataset.href;
if (href) window.location.href = href;
});
});
});
</script>

View File

@@ -88,6 +88,10 @@
<button type="button" class="btn btn-outline-accent btn-sm" id="openCompanyModalBtn">Создать</button>
</div>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Срок отгрузки</label>
<input type="date" class="form-control border-secondary" id="dealDueDate">
</div>
<div class="mb-0">
<label class="form-label small text-muted">Описание</label>
<textarea class="form-control border-secondary" rows="3" id="dealDescription"></textarea>
@@ -157,6 +161,7 @@ document.addEventListener('DOMContentLoaded', function () {
const dealStatus = document.getElementById('dealStatus');
const dealCompany = document.getElementById('dealCompany');
const dealDescription = document.getElementById('dealDescription');
const dealDueDate = document.getElementById('dealDueDate');
const dealSaveBtn = document.getElementById('dealSaveBtn');
const openCompanyModalBtn = document.getElementById('openCompanyModalBtn');
@@ -234,6 +239,7 @@ document.addEventListener('DOMContentLoaded', function () {
dealId.value = '';
dealNumber.value = '';
dealDescription.value = '';
if (dealDueDate) dealDueDate.value = '';
if (dealStatus) dealStatus.value = 'work';
if (dealCompany) dealCompany.value = '';
});
@@ -268,6 +274,7 @@ document.addEventListener('DOMContentLoaded', function () {
status: dealStatus ? dealStatus.value : 'work',
company_id: dealCompany.value,
description: dealDescription.value,
due_date: dealDueDate ? dealDueDate.value : '',
};
await postForm('{% url "deal_upsert" %}', payload);
window.location.reload();

View File

@@ -17,126 +17,752 @@
<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>
{% if deal.status == 'lead' and user_role in 'admin,prod_head,technologist,master,clerk' %}
<form method="post" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="set_work">
<button type="submit" class="btn btn-outline-accent btn-sm">
<i class="bi bi-arrow-right-circle me-1"></i>В работу
</button>
</form>
{% endif %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
{% if user_role in 'admin,prod_head,technologist,master' %}
<form method="post" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="explode_deal">
<button type="submit" class="btn btn-outline-warning btn-sm" title="Пересчитать потребности снабжения">
<i class="bi bi-lightning me-1"></i>Вскрыть BOM
</button>
</form>
{% endif %}
{% if user_role in 'admin,technologist' %}
<a class="btn btn-outline-accent btn-sm" href="{% url 'task_add' %}?deal={{ deal.id }}&next={% url 'planning_deal' deal.id %}">
<i class="bi bi-plus-lg me-1"></i>Добавить деталь
</a>
<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>
{% endif %}
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th>Деталь</th>
<th>Материал</th>
<th>Размер</th>
<th data-sort="false" style="width: 160px;">Прогресс</th>
<th class="text-center">Надо / Сделано / В плане</th>
<th class="text-center">Осталось</th>
<th data-sort="false" class="text-center">Файлы</th>
<th data-sort="false" class="text-end">Действия</th>
</tr>
</thead>
<tbody>
{% for t in tasks %}
<tr class="task-row" style="cursor:pointer" data-href="{% url 'task_items' t.id %}">
<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>
<div class="progress bg-secondary-subtle border border-secondary sf-progress" style="height: 10px;" data-done-width="{{ t.done_width }}" data-plan-width="{{ t.plan_width }}" title="Сделано: {{ t.done_pct }}% · В плане: {{ t.plan_pct }}%">
<div class="progress-bar bg-success sf-progress-done"></div>
<div class="progress-bar bg-warning sf-progress-plan"></div>
</div>
</td>
<td class="text-center">
<span class="text-info fw-bold">{{ t.quantity_ordered }}</span> /
<span class="text-success">{{ t.done_qty }}</span> /
<span class="text-warning">{{ t.planned_qty }}</span>
</td>
<td class="text-center">{{ t.remaining_qty }}</td>
<td class="text-center">
{% if t.drawing_file %}
<a href="{{ t.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/IGES">
<i class="bi bi-file-earmark-code"></i>
</a>
{% endif %}
{% if t.extra_drawing %}
<a href="{{ t.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
<i class="bi bi-file-pdf"></i>
</a>
{% endif %}
</td>
<td class="text-end">
{% if user_role in 'admin,technologist' %}
<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>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center p-5 text-muted">Деталей не найдено</td></tr>
{% endfor %}
</tbody>
</table>
<div class="p-3">
<div class="card border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
<strong>Позиции сделки</strong>
<div class="small text-muted">Изделие / СБ / Деталь</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th>Позиция</th>
<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>
</tr>
</thead>
<tbody>
{% for it in deal_items %}
<tr class="deal-entity-row" role="button" data-href="{% url 'product_info' it.entity.id %}?next={{ request.get_full_path|urlencode }}">
<td>
<div class="fw-bold">{{ it.entity.drawing_number|default:"—" }} {{ it.entity.name }}</div>
<div class="small text-muted">{{ it.entity.get_entity_type_display }}</div>
</td>
<td>
<div class="progress bg-secondary-subtle border border-secondary sf-progress" style="height: 10px;" data-done-width="{{ it.done_width }}" data-plan-width="{{ it.plan_width }}" title="Сделано: {{ it.done_qty }} · В плане: {{ it.planned_qty }}">
<div class="progress-bar bg-success sf-progress-done"></div>
<div class="progress-bar bg-warning sf-progress-plan"></div>
</div>
</td>
<td class="text-center">
<span class="text-info fw-bold">{{ it.quantity }}</span> /
<span class="text-success">{{ it.done_qty }}</span> /
<span class="text-warning">{{ it.planned_qty }}</span>
</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 %}
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Пока нет позиций</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="p-3 pt-0">
<div class="card border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
<strong>Партии поставки</strong>
{% if user_role in 'admin,technologist' %}
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealBatchModal">
<i class="bi bi-plus-lg me-1"></i>Добавить партию
</button>
{% endif %}
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:160px;">Отгрузка</th>
<th>Партия</th>
<th style="width:220px;">Запущено</th>
<th>Состав партии</th>
<th data-sort="false" class="text-end"></th>
</tr>
</thead>
<tbody>
{% for b in delivery_batches %}
<tr>
<td class="fw-bold">{{ b.due_date|date:"d.m.Y" }}</td>
<td>
{{ b.name|default:"—" }}
{% if b.is_default %}
<span class="badge bg-secondary ms-2">по умолчанию</span>
{% endif %}
</td>
<td>
<div class="small text-muted mb-1">{{ b.total_started }} / {{ b.total_qty }} (осталось {{ b.total_remaining }})</div>
<div class="progress bg-secondary-subtle border border-secondary" style="height: 10px;">
<div class="progress-bar bg-warning" style="width: {{ b.started_pct }}%"></div>
</div>
</td>
<td>
{% if b.items_list %}
<div class="table-responsive">
<table class="table table-sm mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:110px;">Тип</th>
<th style="width:160px;">Обозначение</th>
<th>Наименование</th>
<th class="text-center" style="width:80px;">Кол-во</th>
<th class="text-center" style="width:90px;">Запущено</th>
<th class="text-center" style="width:90px;">Осталось</th>
<th style="width:160px;">Прогресс</th>
<th data-sort="false" class="text-end" style="width:160px;"></th>
</tr>
</thead>
<tbody>
{% for bi in b.items_list %}
<tr>
<td class="small text-muted">{{ bi.entity.get_entity_type_display }}</td>
<td class="fw-bold">{{ bi.entity.drawing_number|default:"—" }}</td>
<td>{{ bi.entity.name }}</td>
<td class="text-center">{{ bi.quantity }}</td>
<td class="text-center">{{ bi.started_qty }}</td>
<td class="text-center">{{ bi.remaining_to_start }}</td>
<td>
<div class="progress bg-secondary-subtle border border-secondary" style="height: 10px;">
<div class="progress-bar bg-warning" style="width: {{ bi.started_pct }}%"></div>
</div>
</td>
<td class="text-end">
{% 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">
{% csrf_token %}
<input type="hidden" name="action" value="delete_batch_item">
<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 }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-muted">Пусто</div>
{% endif %}
</td>
<td class="text-end">
{% 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">
{% csrf_token %}
<input type="hidden" name="action" value="delete_batch">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="batch_id" value="{{ b.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
</form>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-3">Партий пока нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% for g in workshop_task_groups %}
<div class="card border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex justify-content-between align-items-center">
<strong>{{ g.name }}</strong>
<div class="small text-muted">{{ g.tasks|length }} задач</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th>Позиция</th>
<th>Операция</th>
<th data-sort="false" style="width: 160px;">Прогресс</th>
<th class="text-center">Заказано / Сделано / В смене</th>
<th class="text-center">Осталось</th>
<th data-sort="false" class="text-center">Файлы</th>
<th data-sort="false" class="text-end">Действия</th>
</tr>
</thead>
<tbody>
{% for t in g.tasks %}
<tr class="task-row" style="cursor:pointer" {% if t.entity_id %}data-href="{% url 'product_info' t.entity_id %}?next={{ request.get_full_path|urlencode }}"{% endif %}>
<td>
<div class="fw-bold">
{% if t.entity %}
{{ t.entity.drawing_number|default:"—" }} {{ t.entity.name }}
{% else %}
{{ t.drawing_name|default:"Б/ч" }}
{% endif %}
</div>
<div class="small text-muted">
{% if t.material %}{{ t.material.full_name|default:t.material.name }}{% else %}—{% endif %}
</div>
</td>
<td class="small">{{ t.current_operation_name|default:"—" }}</td>
<td>
<div class="progress bg-secondary-subtle border border-secondary sf-progress" style="height: 10px;" data-done-width="{{ t.done_width }}" data-plan-width="{{ t.plan_width }}" title="Сделано: {{ t.done_pct }}% · В смене: {{ t.plan_pct }}%">
<div class="progress-bar bg-success sf-progress-done"></div>
<div class="progress-bar bg-warning sf-progress-plan"></div>
</div>
</td>
<td class="text-center">
<span class="text-info fw-bold">{{ t.quantity_ordered }}</span> /
<span class="text-success">{{ t.done_qty }}</span> /
<span class="text-warning">{{ t.planned_qty }}</span>
</td>
<td class="text-center">{{ t.remaining_qty }}</td>
<td class="text-center">
{% if t.drawing_file %}
<a href="{{ t.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/IGES">
<i class="bi bi-file-earmark-code"></i>
</a>
{% endif %}
{% if t.extra_drawing %}
<a href="{{ t.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
<i class="bi bi-file-pdf"></i>
</a>
{% endif %}
</td>
<td class="text-end">
{% if user_role in 'admin,technologist' %}
{% if t.current_operation_id and t.entity_id %}
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#workItemModal"
data-entity-id="{{ t.entity_id }}"
data-operation-id="{{ t.current_operation_id }}"
data-workshop-id="{{ t.current_workshop_id|default:'' }}"
data-workshop-name="{{ t.current_workshop_name|default:'' }}"
data-task-name="{% if t.entity %}{{ t.entity.drawing_number|default:'—' }} {{ t.entity.name }}{% else %}{{ t.drawing_name|default:'Б/ч' }}{% endif %}"
data-operation-name="{{ t.current_operation_name|default:'' }}"
data-task-rem="{{ t.remaining_qty }}"
>
<i class="bi bi-plus-lg me-1"></i>В смену
</button>
{% else %}
<button type="button" class="btn btn-outline-secondary btn-sm" disabled>В смену</button>
{% endif %}
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="7" class="text-center p-4 text-muted">Задач нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% empty %}
<div class="text-center p-5 text-muted">Задач нет</div>
{% endfor %}
</div>
</div>
<div class="modal fade" id="addToPlanModal" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="startProductionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'planning_add' %}" class="modal-content border-secondary">
<form method="post" action="{% url 'deal_batch_action' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="start_batch_item_production">
<input type="hidden" name="deal_id" value="{{ deal.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<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>
<div class="small text-muted mb-2" id="spTitle"></div>
<div class="mb-3">
<label class="form-label small text-muted d-block">Станок</label>
<label class="form-label">Партия</label>
<select class="form-select bg-body text-body border-secondary" name="item_id" id="spBatchItem" required>
{% for b in delivery_batches %}
{% for bi in b.items_list %}
{% if bi.remaining_to_start > 0 %}
<option value="{{ bi.id }}" data-entity-id="{{ bi.entity_id }}" data-rem="{{ bi.remaining_to_start }}">
{{ b.due_date|date:"d.m.Y" }}{% if b.name %} · {{ b.name }}{% endif %} — осталось {{ bi.remaining_to_start }} шт
</option>
{% endif %}
{% endfor %}
{% endfor %}
</select>
<div class="form-text">Если списка нет — сначала создай партию и добавь туда позицию сделки.</div>
</div>
<div class="mb-3">
<label class="form-label">Количество, шт</label>
<input class="form-control bg-body text-body border-secondary" type="number" min="1" name="quantity" id="spQty" required>
</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="spSubmit">Запустить</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="workItemModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="{% url 'workitem_add' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="deal_id" value="{{ deal.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">
<input type="hidden" name="entity_id" id="wiEntityId">
<input type="hidden" name="operation_id" id="wiOperationId">
<div class="small text-muted mb-2" id="wiTitle"></div>
<div class="small text-muted mb-3" id="wiOp"></div>
<input type="hidden" name="workshop_id" id="wiWorkshopId">
<div class="small text-muted mb-3" id="wiWorkshopLabel"></div>
<div class="mb-3">
<label class="form-label small text-muted d-block">Пост (опционально)</label>
<div class="d-flex flex-wrap gap-1" id="machineToggleGroup">
<input type="radio" class="btn-check" name="machine_id" id="m_none" value="">
<label class="btn btn-outline-secondary btn-sm" for="m_none">Без станка</label>
{% for m in machines %}
<input type="radio" class="btn-check" name="machine_id" id="m_{{ m.id }}" value="{{ m.id }}" required>
<input type="radio" class="btn-check" name="machine_id" id="m_{{ m.id }}" value="{{ m.id }}" data-workshop-id="{{ m.workshop_id|default:'' }}">
<label class="btn btn-outline-accent btn-sm" for="m_{{ m.id }}">{{ m.name }}</label>
{% endfor %}
</div>
</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>
<label class="form-label small text-muted">Сколько в смену (шт)</label>
<input type="number" min="1" class="form-control border-secondary" name="quantity_plan" id="wiQty" required>
</div>
<div class="small text-muted" id="modalHint"></div>
<div class="form-check mb-3">
<input class="form-check-input border-secondary" type="checkbox" name="recursive_bom" id="recursiveBomCheck" checked>
<label class="form-check-label small text-muted" for="recursiveBomCheck">
Включить в смену все дочерние компоненты (по всем операциям, строго по БОМу)
</label>
</div>
<div class="small text-muted" id="wiHint"></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" id="wiSubmit">Добавить</button>
</div>
</form>
</div>
</div>
<!-- productInfoModal удалён: паспорт компонента открывается отдельной страницей -->
<div class="d-none" id="productInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<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" id="productInfoBody">
<div class="text-muted">Загрузка...</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="dealBatchModal" 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="create_batch">
<input type="hidden" name="deal_id" value="{{ deal.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="row g-2">
<div class="col-md-6">
<label class="form-label">Отгрузка</label>
<input class="form-control bg-body text-body border-secondary" type="date" name="due_date" required>
</div>
<div class="col-md-6">
<label class="form-label">Название (опц.)</label>
<input class="form-control bg-body text-body border-secondary" name="name" placeholder="Напр. Партия 1">
</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>
</div>
</form>
</div>
</div>
<div class="modal fade" id="dealBatchItemModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form method="post" action="{% url 'deal_batch_action' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="add_batch_item">
<input type="hidden" name="deal_id" value="{{ deal.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="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label">Партия</label>
<select class="form-select bg-body text-body border-secondary" name="batch_id" id="batchSelect" required>
{% for b in delivery_batches %}
{% if not b.is_default %}
<option value="{{ b.id }}">{{ b.due_date|date:"d.m.Y" }}{% if b.name %} · {{ b.name }}{% endif %}</option>
{% endif %}
{% empty %}
<option value="">Сначала создай партию</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Позиция сделки</label>
<select class="form-select bg-body text-body border-secondary" name="entity_id" id="biEntitySelect" required {% if not deal_items %}disabled{% endif %}>
{% if deal_items %}
{% for it in deal_items %}
<option value="{{ it.entity.id }}" data-rem="{{ it.remaining_to_allocate|default:0 }}">{{ it.entity.drawing_number|default:"—" }} {{ it.entity.name }}</option>
{% endfor %}
{% else %}
<option value="">Сначала добавь позиции сделки</option>
{% endif %}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Кол-во, шт</label>
<input class="form-control bg-body text-body border-secondary" name="quantity" id="biQty" value="1" min="1" required>
<div class="form-text" id="biQtyHint"></div>
</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" {% if not delivery_batches or not deal_items %}disabled{% endif %}>Добавить</button>
</div>
</form>
</div>
</div>
<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">
{% csrf_token %}
<input type="hidden" name="deal_id" value="{{ deal.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="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label">Тип</label>
<select class="form-select bg-body text-body border-secondary" id="diType">
<option value="product">Изделие</option>
<option value="assembly">Сборочная единица</option>
<option value="part" selected>Деталь</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" id="diDn" placeholder="Опционально">
</div>
<div class="col-md-4">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" id="diName" placeholder="Напр. Основание">
</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>
</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', () => {
document.querySelectorAll('tr.deal-entity-row[data-href]').forEach(tr => {
tr.addEventListener('click', (e) => {
if (e.target && (e.target.closest('button') || e.target.closest('a') || e.target.closest('form') || e.target.closest('input'))) return;
const url = tr.getAttribute('data-href');
if (url) window.location.href = url;
});
});
const spModal = document.getElementById('startProductionModal');
const spTitle = document.getElementById('spTitle');
const spSelect = document.getElementById('spBatchItem');
const spQty = document.getElementById('spQty');
const spSubmit = document.getElementById('spSubmit');
function spApplyFilter(entityId) {
if (!spSelect) return;
let firstVisible = null;
Array.from(spSelect.options).forEach(opt => {
const eid = opt.getAttribute('data-entity-id');
const visible = eid && String(eid) === String(entityId);
opt.hidden = !visible;
opt.disabled = !visible;
if (visible && !firstVisible) firstVisible = opt;
});
if (firstVisible) {
spSelect.value = firstVisible.value;
const rem = parseInt(firstVisible.getAttribute('data-rem') || '0', 10) || 0;
if (spQty) {
spQty.max = rem > 0 ? String(rem) : '';
spQty.value = rem > 0 ? String(rem) : '';
}
if (spSubmit) spSubmit.disabled = false;
} else {
if (spQty) {
spQty.value = '';
spQty.removeAttribute('max');
}
if (spSubmit) spSubmit.disabled = true;
}
}
if (spSelect) {
spSelect.addEventListener('change', () => {
const opt = spSelect.options[spSelect.selectedIndex];
const rem = opt ? (parseInt(opt.getAttribute('data-rem') || '0', 10) || 0) : 0;
if (spQty) {
spQty.max = rem > 0 ? String(rem) : '';
spQty.value = rem > 0 ? String(rem) : '';
}
});
}
if (spModal) {
spModal.addEventListener('shown.bs.modal', (event) => {
const btn = event.relatedTarget;
const eid = btn ? btn.getAttribute('data-entity-id') : '';
const label = btn ? btn.getAttribute('data-entity-label') : '';
if (spTitle) spTitle.textContent = label || '';
spApplyFilter(eid);
if (spQty) spQty.focus({ preventScroll: true });
});
}
});
document.addEventListener('DOMContentLoaded', () => {
const biModal = document.getElementById('dealBatchItemModal');
const batchSelect = document.getElementById('batchSelect');
const entitySelect = document.getElementById('biEntitySelect');
const qtyEl = document.getElementById('biQty');
const hintEl = document.getElementById('biQtyHint');
function syncRemainingHint() {
if (!entitySelect || !qtyEl) return;
const opt = entitySelect.options[entitySelect.selectedIndex];
const rem = opt ? (parseInt(opt.getAttribute('data-rem') || '0', 10) || 0) : 0;
qtyEl.max = rem > 0 ? String(rem) : '';
if (rem > 0 && (parseInt(qtyEl.value || '0', 10) || 0) > rem) {
qtyEl.value = String(rem);
}
if (hintEl) {
hintEl.textContent = rem > 0 ? `Доступно к распределению: ${rem} шт` : 'Доступно к распределению: 0 шт';
}
}
if (entitySelect) {
entitySelect.addEventListener('change', syncRemainingHint);
}
document.querySelectorAll('[data-bs-target="#dealBatchItemModal"][data-batch-id]').forEach(btn => {
btn.addEventListener('click', () => {
const bid = btn.getAttribute('data-batch-id');
if (batchSelect && bid) batchSelect.value = String(bid);
});
});
if (biModal) {
biModal.addEventListener('shown.bs.modal', () => {
syncRemainingHint();
if (entitySelect) entitySelect.focus({ preventScroll: true });
});
}
});
document.addEventListener('DOMContentLoaded', () => {
const modalEl = document.getElementById('dealItemModal');
const formEl = modalEl ? modalEl.querySelector('form') : null;
const typeEl = document.getElementById('diType');
const dnEl = document.getElementById('diDn');
const nameEl = document.getElementById('diName');
const foundEl = document.getElementById('diFound');
const idEl = document.getElementById('diEntityId');
const btn = document.getElementById('diSearchBtn');
if (!typeEl || !dnEl || !nameEl || !foundEl || !idEl) return;
function setSelectedFromFound() {
idEl.value = foundEl.value || '';
}
async function search(opts = { focusFound: false }) {
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) || [];
foundEl.innerHTML = '';
items.forEach(it => {
const opt = document.createElement('option');
opt.value = String(it.id);
opt.textContent = `${it.type} | ${it.drawing_number || '—'} ${it.name || ''}`;
foundEl.appendChild(opt);
});
if (items.length) {
foundEl.value = String(items[0].id);
setSelectedFromFound();
if (opts && opts.focusFound) {
foundEl.focus({ preventScroll: true });
}
} else {
idEl.value = '';
}
foundEl.onchange = setSelectedFromFound;
return items;
}
if (btn) btn.onclick = () => { search({ focusFound: true }); };
const onEnterSearch = (e) => {
if (e.key !== 'Enter') return;
e.preventDefault();
search({ focusFound: true });
};
dnEl.addEventListener('keydown', onEnterSearch);
nameEl.addEventListener('keydown', onEnterSearch);
foundEl.addEventListener('keydown', (e) => {
if (e.key !== 'Enter') return;
e.preventDefault();
setSelectedFromFound();
if (idEl.value && formEl) {
formEl.requestSubmit();
} else {
search({ focusFound: true });
}
});
if (modalEl) {
modalEl.addEventListener('shown.bs.modal', () => {
setTimeout(() => {
dnEl.focus({ preventScroll: true });
dnEl.select();
}, 0);
});
}
});
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('tr.task-row[data-href]').forEach(function (row) {
row.addEventListener('click', function (e) {
@@ -155,22 +781,39 @@ document.addEventListener('DOMContentLoaded', function () {
if (planEl) planEl.style.width = `${plan}%`;
});
const modal = document.getElementById('addToPlanModal');
const modal = document.getElementById('workItemModal');
if (!modal) return;
modal.addEventListener('shown.bs.modal', function (event) {
const btn = event.relatedTarget;
const taskId = btn.getAttribute('data-task-id');
const name = btn.getAttribute('data-task-name');
const entityId = btn.getAttribute('data-entity-id') || '';
const opId = btn.getAttribute('data-operation-id') || '';
const name = btn.getAttribute('data-task-name') || '';
const opName = btn.getAttribute('data-operation-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} шт` : '';
document.getElementById('wiEntityId').value = entityId;
document.getElementById('wiOperationId').value = opId;
const qty = document.getElementById('modalQty');
document.getElementById('wiTitle').textContent = name;
document.getElementById('wiOp').textContent = opName ? `Операция: ${opName}` : 'Операция: —';
const hint = document.getElementById('wiHint');
if (hint) hint.textContent = rem !== null ? `Осталось: ${rem} шт` : '';
const qty = document.getElementById('wiQty');
qty.value = '';
if (!opId) {
const submit = document.getElementById('wiSubmit');
if (submit) submit.disabled = true;
if (hint) hint.textContent = 'У этой позиции не задан техпроцесс (операции). Добавь операции в паспорте.';
return;
}
const submit = document.getElementById('wiSubmit');
if (submit) submit.disabled = false;
let remInt = null;
if (rem && !isNaN(parseInt(rem, 10))) {
remInt = Math.max(1, parseInt(rem, 10));
@@ -186,27 +829,65 @@ document.addEventListener('DOMContentLoaded', function () {
qty.onkeydown = function (e) {
if (e.key === 'Enter') {
e.preventDefault();
const form = document.querySelector('#addToPlanModal form');
const form = document.querySelector('#workItemModal form');
if (form) form.requestSubmit();
}
};
const radios = Array.from(document.querySelectorAll('input[name="machine_id"]'));
const noneRadio = document.getElementById('m_none');
const wsIdEl = document.getElementById('wiWorkshopId');
const wsLabelEl = document.getElementById('wiWorkshopLabel');
const savedMachine = (() => { try { return localStorage.getItem('planning_machine_id'); } catch (_) { return null; } })();
const wsId = btn.getAttribute('data-workshop-id') || '';
const wsName = btn.getAttribute('data-workshop-name') || '';
if (wsIdEl) wsIdEl.value = wsId;
if (wsLabelEl) {
wsLabelEl.textContent = wsId ? `Цех: ${wsName || '—'}` : 'Цех: —';
}
function setMachineVisible(radio, visible) {
const lbl = document.querySelector(`label[for="${radio.id}"]`);
if (lbl) lbl.style.display = visible ? '' : 'none';
radio.style.display = visible ? '' : 'none';
radio.disabled = !visible;
if (!visible && radio.checked) radio.checked = false;
}
radios.forEach(r => {
if (r.id === 'm_none') {
setMachineVisible(r, true);
return;
}
const mWs = r.getAttribute('data-workshop-id') || '';
setMachineVisible(r, !wsId || (mWs && String(mWs) === String(wsId)));
});
let selected = null;
if (savedMachine) {
selected = radios.find(r => r.value === savedMachine);
selected = radios.find(r => r.value === savedMachine && !r.disabled);
}
if (selected) {
selected.checked = true;
} else if (noneRadio) {
noneRadio.checked = true;
}
if (!selected && radios.length) selected = radios[0];
if (selected) selected.checked = true;
radios.forEach(r => {
r.onchange = function () {
try { localStorage.setItem('planning_machine_id', r.value); } catch (_) {}
if (!r.checked) return;
if (r.value) {
try { localStorage.setItem('planning_machine_id', r.value); } catch (_) {}
} else {
try { localStorage.removeItem('planning_machine_id'); } catch (_) {}
}
};
});
});
});
</script>
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<h3 class="text-accent mb-0"><i class="bi bi-graph-up-arrow me-2"></i>План (сделки)</h3>
<form class="d-flex flex-wrap gap-2 align-items-end" method="get">
<div>
<label class="form-label small text-muted mb-1">Поиск</label>
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="№ сделки / заказчик">
</div>
<button class="btn btn-outline-accent btn-sm" type="submit"><i class="bi bi-search me-1"></i>Показать</button>
<a class="btn btn-outline-accent btn-sm" href="{% url 'planning_stages' %}"><i class="bi bi-arrow-counterclockwise me-1"></i>Сброс</a>
</form>
</div>
<div class="p-3">
{% for c in deal_cards %}
<div class="card border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div class="fw-bold">
<a class="text-decoration-none" href="{% url 'planning_deal' c.deal.id %}">{{ c.deal.number }}</a>
<span class="text-muted small ms-2">{{ c.deal.company.name|default:"—" }}</span>
{% if c.deal.due_date %}<span class="badge bg-secondary ms-2">{{ c.deal.due_date|date:"d.m.Y" }}</span>{% endif %}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th>Позиция</th>
<th class="text-center" style="width:120px;">Заказано</th>
{% for op in op_columns %}
<th class="text-center" style="min-width:140px;">
{{ op.name }}
<div class="small text-muted">{{ op.code }}</div>
</th>
{% endfor %}
<th class="text-center" style="width:140px;">Готово</th>
<th class="text-center" style="width:140px;">Отгружено</th>
</tr>
</thead>
<tbody>
{% for r in c.rows %}
<tr style="cursor:pointer" onclick="window.location.href='{% url 'planning_deal' c.deal.id %}';">
<td class="fw-bold">{{ r.entity }}</td>
<td class="text-center">{{ r.need }}</td>
{% for cell in r.op_cells %}
<td class="text-center">
{% if cell.has %}
<div>{{ cell.done }}/{{ r.need }}</div>
<div class="progress bg-secondary-subtle border border-secondary mt-1" style="height:6px;">
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ cell.pct }}%;"></div>
</div>
{% else %}
{% endif %}
</td>
{% endfor %}
<td class="text-center">
<div>{{ r.ready }}/{{ r.need }}</div>
<div class="progress bg-secondary-subtle border border-secondary mt-1" style="height:6px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ r.ready_pct }}%;"></div>
</div>
</td>
<td class="text-center">
<div>{{ r.shipped }}/{{ r.need }}</div>
<div class="progress bg-secondary-subtle border border-secondary mt-1" style="height:6px;">
<div class="progress-bar bg-primary" role="progressbar" style="width: {{ r.shipped_pct }}%;"></div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% empty %}
<div class="text-muted">Нет сделок по выбранным фильтрам.</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,218 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<div class="w-100">
<div class="d-flex flex-wrap align-items-center gap-3 justify-content-between">
<h3 class="text-accent mb-0"><i class="bi bi-truck me-2"></i>Снабжение</h3>
<form method="get" class="d-flex flex-wrap align-items-center gap-2">
<input type="hidden" name="filtered" value="1">
<div class="d-flex w-100 mb-2 gap-2">
<input type="text" class="form-control form-control-sm border-secondary" name="q" placeholder="Сделка / компонент" value="{{ q }}" style="max-width: 300px;">
<button type="submit" class="btn btn-outline-accent btn-sm">
<i class="bi bi-search me-1"></i>Применить
</button>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'procurement' %}">Сброс</a>
<div class="ms-auto">
<button type="submit" name="print" value="1" class="btn btn-outline-secondary btn-sm" formtarget="_blank">
<i class="bi bi-printer me-1"></i>Печать
</button>
</div>
</div>
<div class="d-flex align-items-center gap-1">
<input type="checkbox" class="btn-check" name="grouped" id="grouped" value="1" {% if grouped %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-accent btn-sm" for="grouped"><i class="bi bi-layers me-1"></i>Группировать</label>
</div>
<div class="d-flex flex-wrap align-items-center gap-1">
<span class="small text-muted me-1">Тип:</span>
{% for code, label in type_choices %}
<input type="checkbox" class="btn-check" name="types" id="type_{{ code }}" value="{{ code }}" {% if code in selected_types %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-sm {% if code == 'raw' %}btn-outline-info{% elif code == 'purchased' %}btn-outline-primary{% elif code == 'casting' %}btn-outline-secondary{% elif code == 'outsourced' %}btn-outline-warning{% else %}btn-outline-secondary{% endif %}" for="type_{{ code }}">{{ label }}</label>
{% endfor %}
</div>
<div class="d-flex flex-wrap align-items-center gap-1">
<span class="small text-muted me-1">Статус:</span>
{% for code, label in status_choices %}
<input type="checkbox" class="btn-check" name="statuses" id="st_{{ code }}" value="{{ code }}" {% if code in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-sm {% if code == 'to_order' %}btn-outline-danger{% elif code == 'ordered' %}btn-outline-warning{% else %}btn-outline-success{% endif %}" for="st_{{ code }}">{{ label }}</label>
{% endfor %}
</div>
</form>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th>Компонент</th>
<th style="width: 140px;">Тип</th>
<th style="width: 140px;">К заказу</th>
<th style="width: 220px;">Сделки</th>
<th style="width: 140px;">Статус</th>
<th style="width: 70px;" data-sort="false"></th>
</tr>
</thead>
<tbody>
{% for r in requirements %}
<tr>
<td>
<div class="fw-semibold">{{ r.component_label }}</div>
</td>
<td class="small">
{% if r.type == 'raw' %}Сырьё{% elif r.type == 'purchased' %}Покупное{% elif r.type == 'casting' %}Литьё{% elif r.type == 'outsourced' %}Аутсорс{% else %}—{% endif %}
</td>
<td class="fw-semibold">
{{ r.required_qty }}{% if r.kind == 'raw' %} {{ r.unit }}{% endif %}
</td>
<td>
{% if r.deals and r.deals|length > 0 %}
{% for dn in r.deals|slice:":3" %}
<span class="badge bg-secondary">{{ dn }}</span>
{% endfor %}
{% if r.deals|length > 3 %}
<span class="badge bg-secondary" title="{{ r.deals|join:", " }}"></span>
{% endif %}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
{% if r.status == 'to_order' %}
<span class="badge bg-danger">К заказу</span>
{% elif r.status == 'ordered' %}
<span class="badge bg-warning text-dark">Заказано</span>
{% else %}
<span class="badge bg-success">Закрыто</span>
{% endif %}
</td>
<td class="text-end">
{% if can_edit and not grouped and r.kind == 'component' %}
{% if r.status == 'to_order' %}
<form method="post" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="mark_ordered">
<input type="hidden" name="pr_id" value="{{ r.obj_id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<button type="submit" class="btn btn-outline-accent btn-sm" title="Отметить как заказано">+</button>
</form>
{% elif r.status == 'ordered' %}
<button
type="button"
class="btn btn-outline-accent btn-sm"
title="Оформить приход"
data-bs-toggle="modal"
data-bs-target="#receiveModal"
data-pr-id="{{ r.obj_id }}"
data-label="{{ r.component_label }}"
data-deal-id="{{ r.deal_id }}"
data-deal-number="{{ r.deals.0 }}"
data-max="{{ r.required_qty }}"
>+</button>
{% else %}
<span class="text-muted"></span>
{% endif %}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="6" class="text-center p-5 text-muted">Потребностей не найдено</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="receiveModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="action" value="receive_component">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<input type="hidden" name="pr_id" id="receivePrId">
<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 small text-muted" id="receiveLabel"></div>
<div class="mb-3">
<label class="form-label">Склад</label>
<select class="form-select border-secondary" name="location_id" required>
{% for loc in locations %}
<option value="{{ loc.id }}">{{ loc.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Сделка (опционально)</label>
<select class="form-select border-secondary" name="deal_id" id="receiveDealId">
<option value="">Свободно</option>
{% for d in deals %}
<option value="{{ d.id }}">{{ d.number }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Количество (шт)</label>
<input class="form-control border-secondary" name="quantity" id="receiveQty" placeholder="Напр. 100" required>
<div class="form-text">Количество уменьшит потребность выбранной строки. При достижении 0 статус станет «Закрыто».</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>
</div>
</form>
</div>
</div>
<script>
(function () {
const modal = document.getElementById('receiveModal');
if (!modal) return;
const prId = document.getElementById('receivePrId');
const label = document.getElementById('receiveLabel');
const dealId = document.getElementById('receiveDealId');
const qty = document.getElementById('receiveQty');
modal.addEventListener('show.bs.modal', function (event) {
const btn = event.relatedTarget;
if (!btn) return;
const id = btn.getAttribute('data-pr-id') || '';
const text = btn.getAttribute('data-label') || '';
const max = btn.getAttribute('data-max') || '';
const dId = btn.getAttribute('data-deal-id') || '';
const dNum = btn.getAttribute('data-deal-number') || '';
if (prId) prId.value = id;
if (label) label.textContent = (text ? ('Компонент: ' + text) : '') + (dNum ? (' · Сделка: ' + dNum) : '');
if (dealId) dealId.value = dId;
if (qty) {
qty.value = max || '';
qty.focus();
qty.select();
}
});
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Список к закупке</title>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; line-height: 1.4; color: #333; margin: 20px; }
h2 { font-size: 18px; margin-bottom: 20px; text-align: center; }
h3 { font-size: 14px; margin-top: 20px; margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 6px 8px; }
th { background-color: #f5f5f5; font-weight: bold; text-align: center; }
td { text-align: left; vertical-align: top; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.marks { height: 26px; }
@media print {
body { margin: 0; }
button { display: none; }
}
</style>
</head>
<body>
<div style="text-align: right; margin-bottom: 20px; display: flex; gap: 10px; justify-content: flex-end;">
<button onclick="window.print()" style="padding: 5px 15px; cursor: pointer;">Распечатать</button>
<button onclick="try { window.close(); } catch (e) {} if (!window.closed) { window.history.back(); }" style="padding: 5px 15px; cursor: pointer;">Закрыть</button>
</div>
<h2>Список к закупке от {% now "d.m.Y" %}</h2>
{% for type_key, items in print_data.items %}
<h3>
{% for code, label in type_labels.items %}
{% if code == type_key %}{{ label }}{% endif %}
{% endfor %}
</h3>
<table>
<thead>
<tr>
<th style="width: 6%;">№ п/п</th>
<th style="width: 24%;">Сделка(и)</th>
<th style="width: 42%;">Наименование</th>
<th style="width: 10%;">Кол-во, шт</th>
<th style="width: 18%;">Отметки</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td class="text-right">{{ forloop.counter }}</td>
<td>
{% if item.deals %}
{{ item.deals|join:", " }}
{% else %}
-
{% endif %}
</td>
<td>{{ item.component_label }}</td>
<td class="text-right">{{ item.required_qty }}</td>
<td class="marks">&nbsp;</td>
</tr>
{% endfor %}
</tbody>
</table>
{% empty %}
<p class="text-center">Нет данных для печати по выбранным фильтрам.</p>
{% endfor %}
</body>
</html>

View File

@@ -4,6 +4,18 @@
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb" class="mb-1">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'products' %}">Изделия</a></li>
{% for bc in breadcrumbs %}
{% if forloop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ bc.label }}</li>
{% else %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.label }}</a></li>
{% endif %}
{% endfor %}
</ol>
</nav>
<h3 class="text-accent mb-0"><i class="bi bi-diagram-3 me-2"></i>{{ entity }}</h3>
<div class="small text-muted mt-1">{{ entity.get_entity_type_display }}</div>
</div>
@@ -14,8 +26,8 @@
<i class="bi bi-plus-lg me-1"></i>Добавить
</button>
{% endif %}
{% if parent_id %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'product_detail' parent_id %}">Назад</a>
{% if back_url %}
<a class="btn btn-outline-secondary btn-sm" href="{{ back_url }}">Назад</a>
{% else %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'products' %}">Назад</a>
{% endif %}
@@ -36,10 +48,20 @@
</thead>
<tbody>
{% for ln in lines %}
<tr class="product-row" role="button" data-info-url="{% url 'product_info' ln.child.id %}">
<tr class="product-row" role="button" data-href="{% url 'product_info' ln.child.id %}?next={{ request.get_full_path|urlencode }}">
<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>
{{ ln.child.name }}
{% if ln.child.entity_type == 'product' or ln.child.entity_type == 'assembly' %}
{% if ln.child.assembly_passport and ln.child.assembly_passport.requires_welding %}
<span class="badge bg-secondary ms-2">Сварка</span>
{% endif %}
{% if ln.child.assembly_passport and ln.child.assembly_passport.requires_painting %}
<span class="badge bg-secondary ms-1">Покраска</span>
{% endif %}
{% endif %}
</td>
<td>
{% if ln.child.passport_filled %}
<i class="bi bi-check-circle-fill text-success" title="Заполнено"></i>
@@ -59,7 +81,9 @@
</td>
<td class="text-end">
<div class="d-flex gap-2 justify-content-end">
<a class="btn btn-outline-accent btn-sm" href="{% url 'product_detail' ln.child.id %}?parent={{ entity.id }}" onclick="event.stopPropagation();">Состав</a>
{% if ln.child.entity_type == 'product' or ln.child.entity_type == 'assembly' %}
<a class="btn btn-outline-accent btn-sm" href="{% url 'product_detail' ln.child.id %}?trail={{ trail_child }}" onclick="event.stopPropagation();">Состав</a>
{% endif %}
<form method="post">
{% csrf_token %}
@@ -225,20 +249,6 @@
</div>
</div>
<div class="modal fade" id="productInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<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" id="productInfoBody">
<div class="text-muted">Загрузка...</div>
</div>
</div>
</div>
</div>
{% if request.GET.open == '1' %}
<script>
document.addEventListener('DOMContentLoaded', () => {
@@ -252,30 +262,10 @@
<script>
document.addEventListener('DOMContentLoaded', () => {
const modalEl = document.getElementById('productInfoModal');
const bodyEl = document.getElementById('productInfoBody');
if (!modalEl || !bodyEl) return;
const modal = new bootstrap.Modal(modalEl);
async function openInfo(url) {
bodyEl.innerHTML = '<div class="text-muted">Загрузка...</div>';
modal.show();
try {
const nextUrl = encodeURIComponent(window.location.pathname + window.location.search);
const sep = url.includes('?') ? '&' : '?';
const res = await fetch(url + sep + 'next=' + nextUrl, { credentials: 'same-origin' });
bodyEl.innerHTML = await res.text();
} catch (e) {
bodyEl.innerHTML = '<div class="alert alert-danger">Не удалось загрузить информацию.</div>';
}
}
document.querySelectorAll('tr.product-row[data-info-url]').forEach(tr => {
document.querySelectorAll('tr.product-row[data-href]').forEach(tr => {
tr.addEventListener('click', () => {
const url = tr.getAttribute('data-info-url');
if (!url) return;
openInfo(url);
const url = tr.getAttribute('data-href');
if (url) window.location.href = url;
});
});
});

View File

@@ -1,8 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'products' %}">Изделия</a></li>
{% for bc in breadcrumbs %}
{% if forloop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ bc.label }}</li>
{% else %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.label }}</a></li>
{% endif %}
{% endfor %}
</ol>
</nav>
<div class="small text-muted">{{ entity.get_entity_type_display }}</div>
</div>
<div class="d-flex gap-2">
{% include 'components/_add_to_deal.html' %}
<a class="btn btn-outline-secondary btn-sm" href="{{ next }}">Назад</a>
</div>
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
{% 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">
@@ -36,14 +63,35 @@
{% endif %}
</div>
{% if not can_edit %}
<div class="col-md-6">
<label class="form-label">Маршрут</label>
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
<option value="">— не указано —</option>
{% for r in routes %}
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
{% endfor %}
</select>
<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="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>
</div>
</div>
<div class="col-md-3">
@@ -80,15 +128,82 @@
</form>
{% if can_edit %}
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
{% csrf_token %}
<input type="hidden" name="action" value="create_route">
<input type="hidden" name="next" value="{{ next }}">
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
</form>
<div class="mt-3">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></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>
</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="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>
{% endif %}
<div class="mt-4">
<div class="fw-bold mb-2">Сварные швы</div>
@@ -110,9 +225,9 @@
<td>{{ s.leg_mm }}</td>
<td>{{ s.length_mm }}</td>
<td>{{ s.quantity }}</td>
<td class="text-end">
<td class="text-end" onclick="event.stopPropagation();">
{% if can_edit %}
<form method="post" action="{% url 'product_info' entity.id %}">
<form method="post" action="{% url 'product_info' entity.id %}" onclick="event.stopPropagation();">
{% csrf_token %}
<input type="hidden" name="action" value="delete_weld_seam">
<input type="hidden" name="next" value="{{ next }}">
@@ -157,4 +272,233 @@
</form>
{% endif %}
</div>
</div>
<hr class="border-secondary my-4">
<div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="fw-bold">Состав</div>
{% if can_edit %}
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#bomAddModal">Добавить компонент</button>
{% endif %}
</div>
<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 data-sort="false" class="text-end"></th>
</tr>
</thead>
<tbody>
{% for ln in bom_lines %}
<tr role="button" style="cursor:pointer" onclick="window.location.href='{% url 'product_info' ln.child.id %}?next={{ request.get_full_path|urlencode }}&trail={{ trail_child|urlencode }}';">
<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" 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 %}
<input type="hidden" name="action" value="bom_update_qty">
<input type="hidden" name="bom_id" value="{{ ln.id }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<input type="hidden" name="next" value="{{ next }}">
<input class="form-control form-control-sm bg-body text-body border-secondary" name="quantity" value="{{ ln.quantity }}" {% if not can_edit %}disabled{% endif %}>
<button class="btn btn-outline-secondary btn-sm" type="submit" {% if not can_edit %}disabled{% endif %}>OK</button>
</form>
</td>
<td class="text-end">
{% if can_edit %}
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="bom_delete_line">
<input type="hidden" name="bom_id" value="{{ ln.id }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Удалить</button>
</form>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Пока нет компонентов</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if can_edit %}
<div class="modal fade" id="bomAddModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<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="border border-secondary rounded p-3 mb-3">
<div class="fw-bold mb-2">Найти существующее</div>
<div class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label">Тип</label>
<select class="form-select bg-body text-body border-secondary" id="bomType">
<option value="assembly">Сборочная единица</option>
<option value="part" selected>Деталь</option>
<option value="purchased">Покупное</option>
<option value="casting">Литьё</option>
<option value="outsourced">Аутсорс</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" id="bomDn" placeholder="Опционально">
</div>
<div class="col-md-4">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" id="bomName" placeholder="Опционально">
</div>
<div class="col-md-2 d-grid">
<button type="button" class="btn btn-outline-secondary" id="bomSearchBtn">Поиск</button>
</div>
</div>
<form method="post" action="{% url 'product_info' entity.id %}" class="row g-2 align-items-end mt-2">
{% csrf_token %}
<input type="hidden" name="action" value="bom_add_existing">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<input type="hidden" name="next" value="{{ next }}">
<div class="col-md-8">
<label class="form-label">Найдено</label>
<select class="form-select bg-body text-body border-secondary" id="bomFound"></select>
<input type="hidden" name="child_id" id="bomChildId" required>
</div>
<div class="col-md-2">
<label class="form-label">Кол-во, шт</label>
<input class="form-control bg-body text-body border-secondary" name="quantity" value="1" required>
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-outline-accent" type="submit">Добавить</button>
</div>
</form>
</div>
<div class="border border-secondary rounded p-3">
<div class="fw-bold mb-2">Создать новое и добавить</div>
<form method="post" action="{% url 'product_info' entity.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="bom_create_and_add">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<input type="hidden" name="next" value="{{ next }}">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Тип</label>
<select class="form-select bg-body text-body border-secondary" name="child_type" id="bomCreateType" required>
<option value="assembly">Сборочная единица</option>
<option value="part" selected>Деталь</option>
<option value="purchased">Покупное</option>
<option value="casting">Литьё</option>
<option value="outsourced">Аутсорс</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Кол-во</label>
<input class="form-control bg-body text-body border-secondary" name="quantity" value="1" required>
</div>
<div class="col-md-6">
<label class="form-label">Обозначение</label>
<input class="form-control bg-body text-body border-secondary" name="drawing_number" placeholder="Опционально">
</div>
<div class="col-md-6">
<label class="form-label">Наименование</label>
<input class="form-control bg-body text-body border-secondary" name="name" required>
</div>
<div class="col-12" id="bomPlannedMaterialBlock">
<label class="form-label">Материал (для детали)</label>
<select class="form-select bg-body text-body border-secondary" name="planned_material_id" id="bomPlannedMaterialSelect">
<option value="">— не указано —</option>
{% for m in materials %}
<option value="{{ m.id }}">{{ m.full_name|default:m.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 d-flex justify-content-end mt-2">
<button class="btn btn-outline-accent" type="submit">Создать и добавить</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const typeEl = document.getElementById('bomType');
const dnEl = document.getElementById('bomDn');
const nameEl = document.getElementById('bomName');
const foundEl = document.getElementById('bomFound');
const idEl = document.getElementById('bomChildId');
const btn = document.getElementById('bomSearchBtn');
function setSelected() {
if (idEl) idEl.value = (foundEl && foundEl.value) ? String(foundEl.value) : '';
}
async function search() {
const params = new URLSearchParams({
entity_type: (typeEl && typeEl.value) || '',
q_dn: (dnEl && dnEl.value) || '',
q_name: (nameEl && nameEl.value) || '',
});
const res = await fetch('{% url "entities_search" %}?' + params.toString(), { credentials: 'same-origin' });
const data = await res.json();
const items = (data && data.results) || [];
foundEl.innerHTML = '';
items.forEach(it => {
const opt = document.createElement('option');
opt.value = String(it.id);
opt.textContent = `${it.type} | ${it.drawing_number || '—'} ${it.name || ''}`;
foundEl.appendChild(opt);
});
if (items.length) {
foundEl.value = String(items[0].id);
setSelected();
} else {
if (idEl) idEl.value = '';
}
foundEl.onchange = setSelected;
}
if (btn) btn.addEventListener('click', search);
const createType = document.getElementById('bomCreateType');
const block = document.getElementById('bomPlannedMaterialBlock');
const matSel = document.getElementById('bomPlannedMaterialSelect');
function syncMat() {
const t = (createType && createType.value) || '';
const isPart = t === 'part';
if (block) block.style.display = isPart ? '' : 'none';
if (matSel) {
matSel.disabled = !isPart;
if (!isPart) matSel.value = '';
}
}
if (createType) {
createType.addEventListener('change', syncMat);
syncMat();
}
});
</script>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'products' %}">Изделия</a></li>
{% for bc in breadcrumbs %}
{% if forloop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ bc.label }}</li>
{% else %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.label }}</a></li>
{% endif %}
{% endfor %}
</ol>
</nav>
<div class="small text-muted">{{ entity.get_entity_type_display }}</div>
</div>
<div class="d-flex gap-2">
{% include 'components/_add_to_deal.html' %}
<a class="btn btn-outline-secondary btn-sm" href="{{ next }}">Назад</a>
</div>
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
{% 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">
@@ -38,15 +65,20 @@
<input class="form-control bg-body text-body border-secondary" name="mass_kg" value="{% if passport and passport.mass_kg %}{{ passport.mass_kg }}{% endif %}" inputmode="decimal" {% if not can_edit %}disabled{% endif %}>
</div>
{% if not can_edit %}
<div class="col-md-3">
<label class="form-label">Маршрут</label>
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
<option value="">— не указано —</option>
{% for r in routes %}
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
{% endfor %}
</select>
<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-6">
<label class="form-label">Чертёж (PDF)</label>
@@ -73,12 +105,81 @@
</form>
{% if can_edit %}
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
{% csrf_token %}
<input type="hidden" name="action" value="create_route">
<input type="hidden" name="next" value="{{ next }}">
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
</form>
<div class="mt-3">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></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>
</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="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>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'products' %}">Изделия</a></li>
{% for bc in breadcrumbs %}
{% if forloop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ bc.label }}</li>
{% else %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.label }}</a></li>
{% endif %}
{% endfor %}
</ol>
</nav>
<div class="small text-muted">{{ entity.get_entity_type_display }}</div>
</div>
<div class="d-flex gap-2">
{% include 'components/_add_to_deal.html' %}
<a class="btn btn-outline-secondary btn-sm" href="{{ next }}">Назад</a>
</div>
</div>
<div class="card-body">
<form method="post" action="{% url 'product_info' entity.id %}" class="container-fluid p-0">
{% 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:'' }}">
<input type="hidden" name="trail" value="{{ request.GET.trail|default:'' }}">
<div class="row g-2">
<div class="col-md-4">
@@ -33,4 +61,7 @@
{% endif %}
</div>
</div>
</form>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'products' %}">Изделия</a></li>
{% for bc in breadcrumbs %}
{% if forloop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ bc.label }}</li>
{% else %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.label }}</a></li>
{% endif %}
{% endfor %}
</ol>
</nav>
<div class="small text-muted">{{ entity.get_entity_type_display }}</div>
</div>
<div class="d-flex gap-2">
{% include 'components/_add_to_deal.html' %}
<a class="btn btn-outline-secondary btn-sm" href="{{ next }}">Назад</a>
</div>
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
{% 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">
@@ -28,15 +55,20 @@
<input class="form-control bg-body text-body border-secondary" name="name" value="{{ entity.name }}" {% if not can_edit %}disabled{% endif %}>
</div>
{% if not can_edit %}
<div class="col-md-6">
<label class="form-label">Маршрут</label>
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
<option value="">— не указано —</option>
{% for r in routes %}
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
{% endfor %}
</select>
<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-6">
<label class="form-label">Чертёж/ТЗ (PDF)</label>
@@ -65,12 +97,81 @@
</form>
{% if can_edit %}
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
{% csrf_token %}
<input type="hidden" name="action" value="create_route">
<input type="hidden" name="next" value="{{ next }}">
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
</form>
<div class="mt-3">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></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>
</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="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>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'products' %}">Изделия</a></li>
{% for bc in breadcrumbs %}
{% if forloop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ bc.label }}</li>
{% else %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.label }}</a></li>
{% endif %}
{% endfor %}
</ol>
</nav>
<div class="small text-muted">{{ entity.get_entity_type_display }}</div>
</div>
<div class="d-flex gap-2">
{% include 'components/_add_to_deal.html' %}
<a class="btn btn-outline-secondary btn-sm" href="{{ next }}">Назад</a>
</div>
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
{% 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">
@@ -38,15 +65,20 @@
</select>
</div>
{% if not can_edit %}
<div class="col-md-6">
<label class="form-label">Маршрут</label>
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
<option value="">— не указано —</option>
{% for r in routes %}
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
{% endfor %}
</select>
<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>
@@ -116,12 +148,81 @@
</form>
{% if can_edit %}
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
{% csrf_token %}
<input type="hidden" name="action" value="create_route">
<input type="hidden" name="next" value="{{ next }}">
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
</form>
<div class="mt-3">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></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>
</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="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>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,35 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-2 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{% url 'products' %}">Изделия</a></li>
{% for bc in breadcrumbs %}
{% if forloop.last %}
<li class="breadcrumb-item active" aria-current="page">{{ bc.label }}</li>
{% else %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.label }}</a></li>
{% endif %}
{% endfor %}
</ol>
</nav>
<div class="small text-muted">{{ entity.get_entity_type_display }}</div>
</div>
<div class="d-flex gap-2">
{% include 'components/_add_to_deal.html' %}
<a class="btn btn-outline-secondary btn-sm" href="{{ next }}">Назад</a>
</div>
</div>
<div class="card-body">
<div class="container-fluid p-0">
<form method="post" action="{% url 'product_info' entity.id %}" enctype="multipart/form-data">
{% 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">
@@ -33,15 +60,20 @@
<input class="form-control bg-body text-body border-secondary" name="gost" value="{% if passport %}{{ passport.gost }}{% endif %}" {% if not can_edit %}disabled{% endif %}>
</div>
{% if not can_edit %}
<div class="col-md-6">
<label class="form-label">Маршрут</label>
<select class="form-select bg-body text-body border-secondary" name="route_id" {% if not can_edit %}disabled{% endif %}>
<option value="">— не указано —</option>
{% for r in routes %}
<option value="{{ r.id }}" {% if entity.route_id == r.id %}selected{% endif %}>{{ r.name }}</option>
{% endfor %}
</select>
<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-6">
<label class="form-label">Чертёж/паспорт (PDF)</label>
@@ -68,12 +100,81 @@
</form>
{% if can_edit %}
<form method="post" action="{% url 'product_info' entity.id %}" class="mt-3 d-flex gap-2 align-items-center">
{% csrf_token %}
<input type="hidden" name="action" value="create_route">
<input type="hidden" name="next" value="{{ next }}">
<input class="form-control bg-body text-body border-secondary" name="route_name" placeholder="Новый маршрут">
<button class="btn btn-outline-secondary" type="submit">Добавить маршрут</button>
</form>
<div class="mt-3">
<div class="fw-bold mb-2">Операции техпроцесса</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th style="width:70px;"></th>
<th>Операция</th>
<th>Цех</th>
<th data-sort="false" class="text-end"></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>
</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="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>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -12,16 +12,20 @@
</button>
{% endif %}
<form class="d-flex gap-2 align-items-center" method="get" action="{% url 'products' %}">
<select class="form-select form-select-sm bg-body text-body border-secondary" name="entity_type" style="min-width: 220px;">
<option value="" {% if not entity_type %}selected{% endif %}>Все типы</option>
<option value="product" {% if entity_type == 'product' %}selected{% endif %}>Готовое изделие</option>
<option value="assembly" {% if entity_type == 'assembly' %}selected{% endif %}>Сборочная единица</option>
<option value="part" {% if entity_type == 'part' %}selected{% endif %}>Деталь</option>
<option value="purchased" {% if entity_type == 'purchased' %}selected{% endif %}>Покупное</option>
<option value="casting" {% if entity_type == 'casting' %}selected{% endif %}>Литьё</option>
<option value="outsourced" {% if entity_type == 'outsourced' %}selected{% endif %}>Аутсорс</option>
</select>
<form class="d-flex flex-wrap gap-2 align-items-center" method="get" action="{% url 'products' %}">
<div class="btn-group" role="group" aria-label="Фильтр типов">
<input type="checkbox" class="btn-check" id="tProduct" name="types" value="product" {% if 'product' in entity_types %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="tProduct">Изделие</label>
<input type="checkbox" class="btn-check" id="tAssembly" name="types" value="assembly" {% if 'assembly' in entity_types %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="tAssembly">СБ</label>
<input type="checkbox" class="btn-check" id="tPart" name="types" value="part" {% if 'part' in entity_types %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="tPart">Деталь</label>
<input type="checkbox" class="btn-check" id="tCasting" name="types" value="casting" {% if 'casting' in entity_types %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="tCasting">Литьё</label>
</div>
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="Поиск (обозначение/наименование)" style="min-width: 320px;">
<button class="btn btn-outline-secondary btn-sm" type="submit"><i class="bi bi-search me-1"></i>Найти</button>
@@ -39,12 +43,11 @@
<th>Наименование</th>
<th>Материал</th>
<th>Заполнен</th>
<th data-sort="false"></th>
</tr>
</thead>
<tbody>
{% for p in products %}
<tr class="product-row" role="button" data-info-url="{% url 'product_info' p.id %}">
<tr class="product-row" role="button" data-href="{% url 'product_info' p.id %}?next={{ request.get_full_path|urlencode }}">
<td class="small text-muted">{{ p.get_entity_type_display }}</td>
<td class="fw-bold">{{ p.drawing_number|default:"—" }}</td>
<td>{{ p.name }}</td>
@@ -62,11 +65,6 @@
<i class="bi bi-circle text-muted" title="Не заполнено"></i>
{% endif %}
</td>
<td class="text-end">
<a class="btn btn-outline-accent btn-sm" href="{% url 'product_detail' p.id %}" onclick="event.stopPropagation();">
{% if p.entity_type == 'product' or p.entity_type == 'assembly' %}Состав{% else %}Открыть{% endif %}
</a>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Пока ничего нет</td></tr>
@@ -115,46 +113,32 @@
</div>
</div>
<div class="modal fade" id="productInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<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" id="productInfoBody">
<div class="text-muted">Загрузка...</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modalEl = document.getElementById('productInfoModal');
const bodyEl = document.getElementById('productInfoBody');
if (!modalEl || !bodyEl) return;
document.querySelectorAll('tr.product-row[data-href]').forEach(tr => {
tr.addEventListener('click', () => {
const url = tr.getAttribute('data-href');
if (url) window.location.href = url;
});
});
const modal = new bootstrap.Modal(modalEl);
const form = document.querySelector('form[action="{% url 'products' %}"][method="get"]');
if (!form) return;
async function openInfo(url) {
bodyEl.innerHTML = '<div class="text-muted">Загрузка...</div>';
modal.show();
try {
const nextUrl = encodeURIComponent(window.location.pathname + window.location.search);
const sep = url.includes('?') ? '&' : '?';
const res = await fetch(url + sep + 'next=' + nextUrl, { credentials: 'same-origin' });
bodyEl.innerHTML = await res.text();
} catch (e) {
bodyEl.innerHTML = '<div class="alert alert-danger">Не удалось загрузить информацию.</div>';
function ensureAtLeastOneTypeChecked() {
const boxes = Array.from(form.querySelectorAll('input[type="checkbox"][name="types"]'));
if (!boxes.length) return;
const anyChecked = boxes.some(b => b.checked);
if (!anyChecked) {
const productBox = boxes.find(b => b.value === 'product');
if (productBox) productBox.checked = true;
}
}
document.querySelectorAll('tr.product-row[data-info-url]').forEach(tr => {
tr.addEventListener('click', () => {
const url = tr.getAttribute('data-info-url');
if (!url) return;
openInfo(url);
form.querySelectorAll('input[type="checkbox"][name="types"]').forEach(cb => {
cb.addEventListener('change', () => {
ensureAtLeastOneTypeChecked();
form.submit();
});
});
});

View File

@@ -7,12 +7,12 @@
<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-list-task me-2"></i>Реестр заданий</h3>
{% if user_role in 'admin,technologist,master' %}
<a class="btn btn-outline-secondary btn-sm" target="_blank" href="{% url 'registry_print' %}?{{ request.GET.urlencode }}">
<a class="btn btn-outline-secondary btn-sm" target="_blank" href="{% url 'registry_workitems_print' %}?{{ request.GET.urlencode }}">
<i class="bi bi-printer me-1"></i>Печать
</a>
{% endif %}
</div>
{% include 'shiftflow/partials/_items_table.html' with items=items %}
{% include 'shiftflow/partials/_workitems_table.html' with workitems=workitems %}
</div>
{% endblock %}

View File

@@ -0,0 +1,86 @@
{% load static %}
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Реестр заданий (WorkItem)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<style>
body { background: #fff; color: #000; }
.print-table { width: 100%; border-collapse: collapse; }
.print-table th, .print-table td { border: 1px solid #000; padding: 4px 6px; font-size: 12px; vertical-align: top; }
.print-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 8px; }
.print-title { font-size: 16px; font-weight: 700; margin: 0; }
.print-meta { font-size: 12px; }
.center { text-align: center; }
@media print {
.no-print { display: none !important; }
.page { page-break-after: always; }
.page:last-child { page-break-after: auto; }
}
</style>
</head>
<body>
<div class="container-fluid my-3 no-print d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" onclick="window.print()">Печать</button>
<button class="btn btn-sm btn-outline-secondary" onclick="window.close()">Закрыть</button>
<div class="ms-auto small text-muted">
{{ printed_at|date:"d.m.Y H:i" }}
</div>
</div>
{% for g in groups %}
<div class="container-fluid page my-3">
<div class="print-header">
<div>
<h1 class="print-title">Реестр заданий</h1>
<div class="print-meta">
Цех: <strong>{{ g.workshop }}</strong>
{% if g.machine %} · Станок: <strong>{{ g.machine }}</strong>{% endif %}
</div>
</div>
<div class="print-meta text-end">
{% if print_date %}Дата: <strong>{{ print_date|date:"d.m.y" }}</strong>{% endif %}
</div>
</div>
<table class="print-table">
<thead>
<tr>
<th style="width:80px;">Дата</th>
<th style="width:90px;">Сделка</th>
<th style="width:180px;">Операция</th>
<th>Позиция</th>
<th style="width:160px;">Материал</th>
<th style="width:80px;" class="center">План</th>
<th style="width:80px;" class="center">Факт</th>
<th style="width:90px;">Статус</th>
</tr>
</thead>
<tbody>
{% for wi in g.items %}
<tr>
<td class="center">{{ wi.date|date:"d.m.y" }}</td>
<td class="center">{{ wi.deal.number|default:"-" }}</td>
<td>{{ wi.operation.name|default:wi.stage|default:"—" }}</td>
<td>{{ wi.entity.drawing_number|default:"—" }} {{ wi.entity.name }}</td>
<td>
{% if wi.entity.planned_material %}
{{ wi.entity.planned_material.full_name|default:wi.entity.planned_material.name }}
{% else %}
{% endif %}
</td>
<td class="center">{{ wi.quantity_plan }}</td>
<td class="center">{{ wi.quantity_done }}</td>
<td>{{ wi.status|default:"planned" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</body>
</html>

View File

@@ -0,0 +1,135 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<nav aria-label="breadcrumb" class="small">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a class="text-decoration-none" href="{% url 'directories' %}">Справочники</a></li>
<li class="breadcrumb-item active" aria-current="page">Марки стали</li>
</ol>
</nav>
<h3 class="text-accent mb-0"><i class="bi bi-grid-3x3-gap me-2"></i>Марки стали</h3>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<form method="get" class="d-flex gap-2 align-items-center">
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="Поиск...">
<button class="btn btn-outline-accent btn-sm" type="submit"><i class="bi bi-search me-1"></i>Найти</button>
<a class="btn btn-outline-accent btn-sm" href="{% url 'steel_grades_catalog' %}"><i class="bi bi-arrow-counterclockwise me-1"></i>Сброс</a>
</form>
<a class="btn btn-outline-accent btn-sm" href="{% url 'directories' %}"><i class="bi bi-arrow-left me-1"></i>Назад</a>
{% if can_edit %}
<button class="btn btn-outline-accent btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#gradeModal" onclick="openGradeCreate()">
<i class="bi bi-plus-lg me-1"></i>Создать
</button>
{% endif %}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th>Название</th>
<th>ГОСТ/ТУ</th>
</tr>
</thead>
<tbody>
{% for g in grades %}
<tr role="button" {% if can_edit %}onclick="openGradeEdit(this)"{% endif %}
data-id="{{ g.id }}" data-name="{{ g.name }}" data-gost="{{ g.gost_standard }}">
<td class="fw-bold">{{ g.name }}</td>
<td>{{ g.gost_standard|default:"—" }}</td>
</tr>
{% empty %}
<tr><td colspan="2" class="text-center text-muted py-4">Нет данных</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="gradeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form class="modal-content border-secondary" onsubmit="event.preventDefault(); saveGrade();">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="gradeModalTitle">Марка стали</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="gradeId">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Название</label>
<input class="form-control bg-body text-body border-secondary" id="gradeName" required {% 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" id="gradeGost" {% if not can_edit %}disabled{% endif %}>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-accent" data-bs-dismiss="modal">Отмена</button>
{% if can_edit %}
<button type="submit" class="btn btn-outline-accent">Сохранить</button>
{% endif %}
</div>
</form>
</div>
</div>
<script>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
function openGradeCreate() {
document.getElementById('gradeModalTitle').textContent = 'Марка стали (создание)';
document.getElementById('gradeId').value = '';
document.getElementById('gradeName').value = '';
document.getElementById('gradeGost').value = '';
new bootstrap.Modal(document.getElementById('gradeModal')).show();
}
function openGradeEdit(tr) {
document.getElementById('gradeModalTitle').textContent = 'Марка стали (правка)';
document.getElementById('gradeId').value = tr.getAttribute('data-id') || '';
document.getElementById('gradeName').value = tr.getAttribute('data-name') || '';
document.getElementById('gradeGost').value = tr.getAttribute('data-gost') || '';
new bootstrap.Modal(document.getElementById('gradeModal')).show();
}
async function saveGrade() {
const fd = new FormData();
fd.append('id', document.getElementById('gradeId').value);
fd.append('name', document.getElementById('gradeName').value);
fd.append('gost_standard', document.getElementById('gradeGost').value);
const res = await fetch("{% url 'steel_grade_upsert' %}", {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': getCookie('csrftoken') },
body: fd,
});
if (!res.ok) {
alert('Не удалось сохранить марку стали');
return;
}
window.location.reload();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,141 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<div>
<nav aria-label="breadcrumb" class="small">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a class="text-decoration-none" href="{% url 'directories' %}">Справочники</a></li>
<li class="breadcrumb-item active" aria-current="page">Номенклатура снабжения</li>
</ol>
</nav>
<h3 class="text-accent mb-0"><i class="bi bi-box-seam me-2"></i>Номенклатура снабжения</h3>
<div class="small text-muted">Покупное / Аутсорс</div>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<a class="btn btn-outline-accent btn-sm" href="{% url 'directories' %}"><i class="bi bi-arrow-left me-1"></i>Назад</a>
{% if can_edit %}
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#createSupplyModal">
<i class="bi bi-plus-lg me-1"></i>Создать
</button>
{% endif %}
<form class="d-flex flex-wrap gap-2 align-items-center" method="get" action="{% url 'supply_catalog' %}">
<div class="btn-group" role="group" aria-label="Фильтр типов">
{% for code, label in type_choices %}
<input type="checkbox" class="btn-check" id="t{{ code }}" name="types" value="{{ code }}" {% if code in entity_types %}checked{% endif %}>
<label class="btn btn-outline-secondary btn-sm" for="t{{ code }}">{{ label }}</label>
{% endfor %}
</div>
<input class="form-control form-control-sm bg-body text-body border-secondary" name="q" value="{{ q }}" placeholder="Поиск (обозначение/наименование)" style="min-width: 320px;">
<button class="btn btn-outline-accent btn-sm" type="submit"><i class="bi bi-search me-1"></i>Найти</button>
<a class="btn btn-outline-accent btn-sm" href="{% url 'supply_catalog' %}"><i class="bi bi-arrow-counterclockwise me-1"></i>Сброс</a>
</form>
</div>
</div>
<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>Заполнен</th>
</tr>
</thead>
<tbody>
{% for p in items %}
<tr class="product-row" role="button" data-href="{% url 'product_info' p.id %}?next={{ request.get_full_path|urlencode }}">
<td class="small text-muted">{{ p.get_entity_type_display }}</td>
<td class="fw-bold">{{ p.drawing_number|default:"—" }}</td>
<td>{{ p.name }}</td>
<td>
{% if p.passport_filled %}
<i class="bi bi-check-circle-fill text-success" title="Заполнено"></i>
{% else %}
<i class="bi bi-circle text-muted" title="Не заполнено"></i>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-4">Пока ничего нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="createSupplyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form method="post" action="{% url 'supply_catalog' %}" class="modal-content border-secondary">
{% csrf_token %}
<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">
<div class="col-md-4">
<label class="form-label">Тип</label>
<select class="form-select" name="entity_type" required>
<option value="purchased">Покупное</option>
<option value="outsourced">Аутсорс</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Обозначение/Артикул</label>
<input class="form-control" name="drawing_number" placeholder="Напр. МАСКА-001">
</div>
<div class="col-md-4">
<label class="form-label">Наименование</label>
<input class="form-control" name="name" placeholder="Напр. Маска сварочная хамелеон" required>
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-accent" data-bs-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-outline-accent">Создать</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('tr.product-row[data-href]').forEach(tr => {
tr.addEventListener('click', () => {
const url = tr.getAttribute('data-href');
if (url) window.location.href = url;
});
});
const form = document.querySelector('form[action="{% url 'supply_catalog' %}"][method="get"]');
if (!form) return;
function ensureAtLeastOneTypeChecked() {
const boxes = Array.from(form.querySelectorAll('input[type="checkbox"][name="types"]'));
if (!boxes.length) return;
const anyChecked = boxes.some(b => b.checked);
if (!anyChecked) {
boxes[0].checked = true;
}
}
form.querySelectorAll('input[type="checkbox"][name="types"]').forEach(cb => {
cb.addEventListener('change', () => {
ensureAtLeastOneTypeChecked();
form.submit();
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,308 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card shadow-sm border-secondary mb-4">
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
<h3 class="text-accent mb-0">
<i class="bi bi-info-circle me-2"></i>
<a href="{% url 'workitem_entity_list' workitem.deal.id workitem.entity.id %}" class="text-decoration-none text-reset">
{{ workitem.entity.drawing_number|default:"—" }} {{ workitem.entity.name }}
</a>
{% if can_edit_entity %}
<a class="ms-2 text-decoration-none" href="{% url 'product_info' workitem.entity.id %}?next={{ request.path|urlencode }}" title="Открыть паспорт">
<i class="bi bi-pencil-square"></i>
</a>
{% endif %}
</h3>
<div class="d-flex align-items-center gap-2">
<a href="{% url 'planning_deal' workitem.deal.id %}" class="text-decoration-none">
<span class="badge bg-secondary">Сделка № {{ workitem.deal.number }}</span>
</a>
{% if user_role in 'admin,master,operator,prod_head' and close_url %}
<a class="btn btn-outline-warning btn-sm" href="{{ close_url }}">
<i class="bi bi-check2-square me-1"></i>{{ close_label|default:'Закрыть' }}
</a>
{% endif %}
{% if user_role in 'admin,technologist,master,clerk' %}
{% if workitem.entity.entity_type == 'product' or workitem.entity.entity_type == 'assembly' %}
<a class="btn btn-outline-accent btn-sm" href="{% url 'workitem_kitting' workitem.id %}?next={{ request.get_full_path|urlencode }}">
<i class="bi bi-box-seam me-1"></i>Комплектация
</a>
{% endif %}
{% endif %}
</div>
</div>
<div class="card-body p-4">
<form method="post" action="{% url 'workitem_update' %}" class="mb-4" id="workitemForm">
{% csrf_token %}
<input type="hidden" name="workitem_id" value="{{ workitem.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body">
<div class="col-md-4">
<small class="text-muted d-block">Дата</small>
{% if user_role in 'admin,technologist' %}
<input type="date" class="form-control border-secondary" name="date" value="{{ workitem.date|date:'Y-m-d' }}">
{% else %}
<strong>{{ workitem.date|date:"d.m.Y" }}</strong>
{% endif %}
</div>
<div class="col-md-4">
<small class="text-muted d-block">Цех/Пост</small>
{% if user_role in 'admin,technologist,master' %}
<select name="machine_id" class="form-select border-secondary">
<option value="">— без станка —</option>
{% for m in machines %}
<option value="{{ m.id }}" {% if workitem.machine_id == m.id %}selected{% endif %}>{{ m.name }}</option>
{% endfor %}
</select>
{% else %}
<strong>
{% if workitem.machine %}{{ workitem.machine.name }}{% elif workitem.workshop %}{{ workitem.workshop.name }}{% else %}—{% endif %}
</strong>
{% endif %}
</div>
<div class="col-md-4">
<small class="text-muted d-block">Операция</small>
<strong>{% if workitem.operation %}{{ workitem.operation.name }}{% else %}{{ workitem.stage|default:"—" }}{% endif %}</strong>
</div>
</div>
<div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body">
<div class="col-md-4">
<small class="text-muted d-block">Материал (паспорт)</small>
<strong>
{% if workitem.entity.planned_material %}
{{ workitem.entity.planned_material.full_name|default:workitem.entity.planned_material.name }}
{% else %}
{% endif %}
</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">План</small>
{% if user_role in 'admin,technologist' %}
<input type="number" min="0" class="form-control border-secondary" name="quantity_plan" value="{{ workitem.quantity_plan }}">
{% else %}
<strong class="text-info fs-5">{{ workitem.quantity_plan }} шт.</strong>
{% endif %}
</div>
<div class="col-md-4">
<small class="text-muted d-block">Факт</small>
{% if user_role in 'admin,technologist,master,operator' %}
<input type="number" min="0" class="form-control border-secondary" name="quantity_done" id="wiDone" value="{{ workitem.quantity_done }}">
{% else %}
<strong class="text-success fs-5">{{ workitem.quantity_done }} шт.</strong>
{% endif %}
</div>
</div>
<div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body">
<div class="col-md-4">
<small class="text-muted d-block">Статус задания</small>
{% if user_role in 'admin,technologist' %}
<select class="form-select border-secondary" name="workitem_status">
{% for val, label in workitem_status_choices %}
<option value="{{ val }}" {% if workitem.status == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% else %}
<strong>{{ workitem.get_status_display }}</strong>
{% endif %}
</div>
<div class="col-12">
{% if workitem.comment %}
<div class="alert alert-warning border border-warning sf-attention mb-3">
<div class="d-flex align-items-start gap-2">
<i class="bi bi-exclamation-triangle"></i>
<div class="fw-bold">{{ workitem.comment }}</div>
</div>
</div>
{% endif %}
{% if user_role in 'admin,technologist,master' %}
<small class="text-muted d-block">Комментарий</small>
<textarea class="form-control border-secondary" name="comment" rows="2" placeholder="Указания/заметки">{{ workitem.comment }}</textarea>
{% endif %}
</div>
</div>
</form>
{% if workitem.entity.entity_type == 'part' %}
<div class="mb-4">
<div class="small text-muted mb-2">Превью</div>
<div class="row g-3">
<div class="col-md-8">
<div class="border border-secondary rounded p-2" style="height: 200px; overflow: hidden;">
{% if workitem.entity.preview %}
<img src="{{ workitem.entity.preview.url }}" alt="Превью" style="max-width:100%; max-height:100%; object-fit:contain; display:block; margin:0 auto;">
{% else %}
<div style="width:100%; height:100%;"></div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<div class="d-flex align-items-center gap-2 mb-2">
{% if workitem.entity.dxf_file %}
<a href="{{ workitem.entity.dxf_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1" title="DXF/STEP">
<i class="bi bi-file-earmark-code"></i>
</a>
{% else %}
<span class="btn btn-sm btn-outline-secondary p-1 disabled" title="DXF/STEP">
<i class="bi bi-file-earmark-code"></i>
</span>
{% endif %}
<div class="small text-muted">DXF/STEP</div>
</div>
</div>
<div>
<div class="d-flex align-items-center gap-2 mb-2">
{% if workitem.entity.pdf_main %}
<a href="{{ workitem.entity.pdf_main.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1" title="PDF">
<i class="bi bi-file-pdf"></i>
</a>
{% else %}
<span class="btn btn-sm btn-outline-secondary p-1 disabled" title="PDF">
<i class="bi bi-file-pdf"></i>
</span>
{% endif %}
<div class="small text-muted">PDF</div>
</div>
</div>
</div>
{% if can_edit_entity and workitem.entity.dxf_file %}
<form method="post" action="{% url 'product_preview_update' workitem.entity.id %}" class="mt-3">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button type="submit" class="btn btn-outline-secondary btn-sm">
{% if workitem.entity.preview %}Обновить превью DXF{% else %}Сгенерировать превью DXF{% endif %}
</button>
</form>
{% endif %}
</div>
</div>
{% else %}
<div class="mb-4">
<div class="small text-muted mb-2">Файлы</div>
<div class="d-flex gap-2">
{% if workitem.entity.dxf_file %}
<a href="{{ workitem.entity.dxf_file.url }}" target="_blank" class="btn btn-sm btn-outline-info" title="DXF/STEP">
<i class="bi bi-file-earmark-code me-1"></i>DXF/STEP
</a>
{% endif %}
{% if workitem.entity.pdf_main %}
<a href="{{ workitem.entity.pdf_main.url }}" target="_blank" class="btn btn-sm btn-outline-danger" title="PDF">
<i class="bi bi-file-pdf me-1"></i>PDF
</a>
{% endif %}
{% if not workitem.entity.dxf_file and not workitem.entity.pdf_main %}
<div class="text-muted"></div>
{% endif %}
</div>
</div>
{% endif %}
{% if workitem.entity.entity_type == 'product' or workitem.entity.entity_type == 'assembly' %}
<div class="mb-4">
<div class="fw-bold mb-2">Паспорт сборки</div>
<div class="row g-2">
<div class="col-md-4">
<div class="small text-muted">Масса, кг</div>
<div>{{ passport.weight_kg|default:"—" }}</div>
</div>
<div class="col-md-4">
<div class="small text-muted">Покрытие</div>
<div>{{ passport.coating|default:"—" }}</div>
</div>
<div class="col-md-4">
<div class="small text-muted">Цвет</div>
<div>{{ passport.coating_color|default:"—" }}</div>
</div>
<div class="col-md-4">
<div class="small text-muted">Площадь покрытия, м²</div>
<div>{{ passport.coating_area_m2|default:"—" }}</div>
</div>
<div class="col-md-4">
<div class="small text-muted">Сварка</div>
<div>{% if passport.requires_welding %}Да{% else %}Нет{% endif %}</div>
</div>
<div class="col-md-4">
<div class="small text-muted">Покраска</div>
<div>{% if passport.requires_painting %}Да{% else %}Нет{% endif %}</div>
</div>
<div class="col-12">
<div class="small text-muted">Технические требования</div>
<div class="border border-secondary rounded p-2 bg-body">{{ passport.technical_requirements|default:"—"|linebreaksbr }}</div>
</div>
</div>
</div>
<div class="mb-4">
<div class="fw-bold mb-2">Сварочные швы</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th>Наименование</th>
<th class="text-center" style="width:120px;">Катет, мм</th>
<th class="text-center" style="width:120px;">Длина, мм</th>
<th class="text-center" style="width:120px;">Кол-во</th>
</tr>
</thead>
<tbody>
{% for s in welding_seams %}
<tr>
<td>{{ s.name }}</td>
<td class="text-center">{{ s.leg_mm }}</td>
<td class="text-center">{{ s.length_mm }}</td>
<td class="text-center">{{ s.quantity }}</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-3">Швов нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="d-flex justify-content-between mt-4">
<a href="{{ back_url }}" class="btn btn-outline-secondary">Назад</a>
{% if user_role in 'admin,technologist,master,operator' %}
<button type="button" class="btn btn-outline-accent px-4 fw-bold" onclick="document.getElementById('workitemForm')?.requestSubmit()">
<i class="bi bi-save me-2"></i>Сохранить
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const done = document.getElementById('wiDone');
const form = document.getElementById('workitemForm');
if (done) {
done.focus({ preventScroll: true });
done.select();
done.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
if (form) form.requestSubmit();
}
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% 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-list-task me-2"></i>{{ entity.drawing_number|default:"—" }} {{ entity.name }}
</h3>
<div class="small text-muted">
Сделка {{ deal.number }}
</div>
</div>
<div class="d-flex gap-2">
{% if can_edit_entity %}
<a class="btn btn-outline-secondary btn-sm" href="{% url 'product_info' entity.id %}?next={{ request.path|urlencode }}">
<i class="bi bi-pencil-square me-1"></i>Паспорт
</a>
{% endif %}
<a class="btn btn-outline-secondary btn-sm" href="{{ back_url }}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
</div>
</div>
{% include 'shiftflow/partials/_workitems_table.html' with workitems=workitems %}
</div>
{% endblock %}

View File

@@ -0,0 +1,117 @@
{% extends 'base.html' %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 class="text-accent mb-1">
<i class="bi bi-box-seam me-2"></i>Комплектация
</h3>
<div class="small text-muted">
Сделка № {{ workitem.deal.number }} · {{ workitem.entity.drawing_number|default:"—" }} {{ workitem.entity.name }}
</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-secondary btn-sm" href="{{ back_url }}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
{% if draft %}
<a class="btn btn-outline-accent btn-sm" href="{% url 'workitem_kitting_print' workitem.id %}" target="_blank" rel="noopener">
<i class="bi bi-printer me-1"></i>Печать
</a>
<form method="post" class="d-inline">
{% csrf_token %}
<input type="hidden" name="action" value="clear">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<button class="btn btn-outline-secondary btn-sm" type="submit">Очистить лист</button>
</form>
{% endif %}
</div>
</div>
<div class="row g-3">
<div class="col-12">
<div class="card shadow-sm border-secondary">
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
<div class="fw-bold">Потребные компоненты</div>
<div class="small text-muted">
{% if to_location %}
Куда: {{ to_location.name }} · Кол-во: {{ qty_to_make }} шт
{% else %}
Склад участка не определён
{% endif %}
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th>Компонент</th>
<th class="text-center" style="width:110px;">Нужно</th>
<th class="text-center" style="width:130px;">Есть на участке</th>
<th class="text-center" style="width:130px;">К перемещению</th>
<th class="text-center" style="width:110px;">Не хватает</th>
<th style="width:380px;">Откуда взять</th>
</tr>
</thead>
<tbody>
{% for r in 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.need }}</td>
<td class="text-center">{{ r.have_to }}</td>
<td class="text-center">{{ r.to_move }}</td>
<td class="text-center">
{% if r.missing > 0 %}
<span class="text-danger fw-bold">{{ r.missing }}</span>
{% else %}
<span class="text-success">0</span>
{% endif %}
</td>
<td>
{% if r.sources %}
<div class="d-grid gap-2">
{% for s in r.sources %}
<form method="post" class="border border-secondary rounded p-2">
{% csrf_token %}
<input type="hidden" name="entity_id" value="{{ r.entity.id }}">
<input type="hidden" name="from_location_id" value="{{ s.location.id }}">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold">{{ s.location.name }}</div>
<div class="small text-muted">Доступно: {{ s.available }}{% if s.selected %} · В перемещении: {{ s.selected }}{% endif %}</div>
</div>
<div class="d-flex align-items-center gap-2">
<input class="form-control form-control-sm border-secondary" style="width:110px;" type="number" min="1" name="quantity" value="{% if r.missing > 0 %}{{ r.missing }}{% else %}1{% endif %}">
<button class="btn btn-outline-accent btn-sm" type="submit" name="action" value="add_line">В перемещение</button>
<button class="btn btn-outline-secondary btn-sm" type="submit" name="action" value="remove_line" {% if not s.selected %}disabled{% endif %}>Откатить</button>
</div>
</div>
</form>
{% endfor %}
</div>
{% else %}
<div class="text-muted">Нет остатков (под сделку/свободных)</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="6" class="text-center text-muted py-4">Нет потребных компонентов (или кол-во = 0)</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer border-secondary small text-muted">
В таблице показываются только остатки под эту сделку и свободные. «Не хватает» пересчитывается с учетом колонки «К перемещению».
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,75 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Перемещение</title>
<style>
body { font-family: Arial, sans-serif; background: #fff; color: #000; }
.page { width: 210mm; margin: 10mm auto; }
.actions { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: 8px; }
.btn { border: 1px solid #000; background: #fff; padding: 6px 10px; font-size: 12px; cursor: pointer; }
h1 { font-size: 18px; margin: 0 0 6px 0; }
.meta { font-size: 12px; margin: 0 0 12px 0; }
.block-title { font-weight: 700; margin: 14px 0 6px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #000; padding: 6px 8px; font-size: 12px; }
th { text-align: center; font-weight: 700; }
td.num, th.num { text-align: right; width: 14mm; }
td.qty, th.qty { text-align: right; width: 30mm; }
td.note, th.note { width: 45mm; }
td.name { text-align: left; }
@media print { .actions { display:none; } .page { margin: 0; width: auto; } }
</style>
</head>
<body>
<div class="page">
<div class="actions">
<button type="button" class="btn" onclick="window.close()">Закрыть</button>
<form method="post" style="margin:0">
{% csrf_token %}
<button type="submit" name="action" value="apply" class="btn">Принять</button>
</form>
<form method="post" style="margin:0">
{% csrf_token %}
<button type="submit" name="action" value="apply_print" class="btn">Распечатать</button>
</form>
</div>
<h1>Перемещение на {{ to_location.name }}</h1>
<div class="meta">Сделка № {{ workitem.deal.number }} · {{ workitem.entity.drawing_number|default:"—" }} {{ workitem.entity.name }} · {{ printed_at|date:"d.m.Y H:i" }}</div>
{% for g in groups %}
<div class="block-title">С {{ g.from_location.name }}</div>
<table>
<thead>
<tr>
<th class="num"></th>
<th>Наименование</th>
<th class="qty">К перемещению, шт</th>
<th class="note">Отметка</th>
</tr>
</thead>
<tbody>
{% for it in g.items %}
<tr>
<td class="num">{{ forloop.counter }}</td>
<td class="name">{{ it.entity.drawing_number|default:"—" }} {{ it.entity.name }}</td>
<td class="qty">{{ it.quantity }}</td>
<td class="note"></td>
</tr>
{% endfor %}
</tbody>
</table>
{% empty %}
<div class="meta">Лист перемещения пуст.</div>
{% endfor %}
</div>
{% if auto_print %}
<script>
window.print();
</script>
{% endif %}
</body>
</html>

View File

@@ -0,0 +1,47 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm border-secondary mb-4">
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
<h3 class="text-accent mb-0">
<i class="bi bi-check2-square me-2"></i>Закрытие операции
</h3>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'workitem_detail' workitem.id %}">Назад к заданию</a>
</div>
<div class="card-body p-4">
<div class="mb-3">
<div class="fw-bold">{{ workitem.entity.drawing_number|default:"—" }} {{ workitem.entity.name }}</div>
<div class="small text-muted">Сделка № {{ workitem.deal.number }}</div>
<div class="small text-muted">
Операция: {% if workitem.operation %}{{ workitem.operation.name }}{% else %}{{ workitem.stage|default:"—" }}{% endif %}
</div>
<div class="small text-muted">План: {{ workitem.quantity_plan }} · Факт: {{ workitem.quantity_done }} · Остаток: <strong>{{ remaining }}</strong></div>
</div>
<div class="alert alert-info border-info">
Эта операция не списывает сырьё/комплектующие. Здесь фиксируется только факт выполнения.
</div>
<form method="post">
{% csrf_token %}
<div class="row align-items-end g-2">
<div class="col-md-6">
<label class="form-label text-muted small mb-1">Фактически выполнено (шт.)</label>
<input type="number" class="form-control border-secondary" name="fact_qty" min="1" max="{{ remaining }}" value="{{ remaining }}" {% if remaining == 0 %}disabled{% endif %}>
</div>
<div class="col-md-6">
<button type="submit" class="btn btn-warning w-100" {% if remaining == 0 %}disabled{% endif %}>
Закрыть операцию
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary mb-3">
<div class="card-header border-secondary py-3 d-flex flex-wrap gap-2 justify-content-between align-items-center">
<h3 class="text-accent mb-0"><i class="bi bi-building me-2"></i>Справочник · Цеха</h3>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'directories' %}">Назад</a>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle" data-sortable="1">
<thead>
<tr class="table-custom-header">
<th class="text-center" style="width:80px;" data-sort-type="number"></th>
<th class="text-center" style="width:90px;" data-sort-type="number">ID</th>
<th>Цех</th>
<th>Склад цеха</th>
<th>Посты/станки</th>
</tr>
</thead>
<tbody>
{% for ws in workshops %}
<tr style="cursor:pointer" onclick="window.location.href='{% url 'machines_catalog' %}?workshop_id={{ ws.id }}';">
<td class="text-center">{{ forloop.counter }}</td>
<td class="text-center text-muted">{{ ws.id }}</td>
<td class="fw-bold">{{ ws.name }}</td>
<td>{{ ws.location.name|default:"—" }}</td>
<td class="small">
{% if ws.machine_labels %}
{{ ws.machine_labels }}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="text-center text-muted py-4">Цехов нет.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -32,14 +32,30 @@
<div class="small text-muted">По производственным отчетам</div>
</div>
<div class="card-body">
{% for card in report_cards %}
<form method="post" class="mb-0">
{% csrf_token %}
<input type="hidden" name="start_date" value="{{ start_date }}">
<input type="hidden" name="end_date" value="{{ end_date }}">
<div class="card-body">
{% for card in report_cards %}
<div class="border border-secondary rounded p-3 mb-3">
<div class="d-flex flex-wrap justify-content-between gap-2">
<div class="d-flex flex-wrap justify-content-between gap-2 align-items-center">
<div class="fw-bold">
{{ card.report.date|date:"d.m.Y" }} — {{ card.report.machine }} — {{ card.report.operator }}
<span class="text-muted small ms-2">#{{ card.report.id }}</span>
</div>
<div class="d-flex align-items-center gap-2">
{% if card.report.is_synced_1c %}
<span class="badge bg-success">Выгружено в 1С</span>
{% else %}
<span class="badge bg-secondary">Не выгружено</span>
{% if can_edit %}
<input class="form-check-input" type="checkbox" name="report_ids" value="{{ card.report.id }}" title="Отметить выгружено в 1С">
{% endif %}
{% endif %}
</div>
</div>
<div class="row g-3 mt-1">
@@ -54,6 +70,12 @@
({% if c.stock_item.current_length and c.stock_item.current_width %}{{ c.stock_item.current_length|floatformat:"-g" }}×{{ c.stock_item.current_width|floatformat:"-g" }}{% elif c.stock_item.current_length %}{{ c.stock_item.current_length|floatformat:"-g" }}{% else %}—{% endif %})
{{ c.quantity|floatformat:"-g" }} шт
</li>
{% elif c.stock_item_id and c.stock_item.entity_id %}
<li>
{{ c.stock_item.entity }}
{% if c.stock_item.deal_id %}<span class="text-muted">(сделка № {{ c.stock_item.deal.number }})</span>{% endif %}
{{ c.quantity|floatformat:"-g" }} шт
</li>
{% elif c.material_id %}
<li>{{ c.material }} {{ c.quantity|floatformat:"-g" }} шт</li>
{% else %}
@@ -81,10 +103,14 @@
<div class="col-lg-4">
<div class="small text-muted fw-bold mb-1">Остаток ДО</div>
{% if card.remnants %}
{% if card.report.remnants.all %}
<ul class="mb-0">
{% for k,v in card.remnants.items %}
<li>{{ k }}: {{ v }} шт</li>
{% for r in card.report.remnants.all %}
<li>
{{ r.material.full_name|default:r.material.name|default:r.material }}
({% if r.current_length and r.current_width %}{{ r.current_length|floatformat:"-g" }}×{{ r.current_width|floatformat:"-g" }}{% elif r.current_length %}{{ r.current_length|floatformat:"-g" }}{% else %}—{% endif %})
{{ r.quantity|floatformat:"-g" }} шт
</li>
{% endfor %}
</ul>
{% else %}
@@ -93,79 +119,16 @@
</div>
</div>
</div>
{% empty %}
<div class="text-muted">За выбранный период отчётов нет.</div>
{% endfor %}
</div>
</div>
<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-check2-square me-2"></i>Сменные задания (1С)</h3>
<div class="small text-muted">Отметка «Списано в 1С»</div>
</div>
<form method="post" class="mb-0">
{% csrf_token %}
<input type="hidden" name="start_date" value="{{ start_date }}">
<input type="hidden" name="end_date" value="{{ end_date }}">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th data-sort="false"></th>
<th>Дата</th>
<th>Сделка</th>
<th>Станок</th>
<th>Деталь</th>
<th>Статус</th>
<th>Факт</th>
<th>1С</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<tr>
<td style="width:40px;">
{% if can_edit and not it.is_synced_1c %}
<input class="form-check-input" type="checkbox" name="item_ids" value="{{ it.id }}">
{% endif %}
</td>
<td class="small">{{ it.date|date:"d.m.Y" }}</td>
<td>
{% if it.task.deal_id %}
<span class="text-accent fw-bold">{{ it.task.deal.number }}</span>
{% else %}
{% endif %}
</td>
<td>{{ it.machine }}</td>
<td>
<a href="{% url 'item_detail' it.id %}" class="text-decoration-none">{{ it.task.drawing_name }}</a>
</td>
<td>{{ it.get_status_display }}</td>
<td>{{ it.quantity_fact }}</td>
<td>
{% if it.is_synced_1c %}
<span class="badge bg-success">Да</span>
{% else %}
<span class="badge bg-secondary">Нет</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center text-muted py-4">Нет закрытых заданий за период</td></tr>
{% endfor %}
</tbody>
</table>
{% empty %}
<div class="text-muted">За выбранный период отчётов нет.</div>
{% endfor %}
</div>
{% if can_edit %}
<div class="card-footer border-secondary d-flex justify-content-end">
<button type="submit" class="btn btn-outline-accent" {% if not can_edit %}disabled{% endif %}>
Отметить выбранные как «Списано в 1С»
</button>
<button type="submit" class="btn btn-outline-accent">Отметить выбранные как «Выгружено в 1С»</button>
</div>
{% endif %}
</form>
</div>
{% endblock %}