Огромная замена логики
All checks were successful
Deploy MES Core / deploy (push) Successful in 11s

This commit is contained in:
2026-04-06 08:06:37 +03:00
parent 0e8497ab1f
commit e88b861f68
48 changed files with 3833 additions and 175 deletions

View File

@@ -0,0 +1,560 @@
{% extends 'base.html' %}
{% block content %}
<div class="card border-secondary mb-3 shadow-sm">
<div class="card-body py-2">
<form method="get" id="warehouse-filter-form" class="row g-2 align-items-center">
<input type="hidden" name="q" value="{{ q }}">
<div class="col-md-7">
<div class="small text-muted mb-1 fw-bold">Склады:</div>
<div class="d-flex flex-wrap gap-1">
<div>
<input type="radio" class="btn-check" name="location_id" id="wl_all" value="" {% if not selected_location_id %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-accent btn-sm" for="wl_all">Все</label>
</div>
{% for loc in locations %}
<div>
<input type="radio" class="btn-check" name="location_id" id="wl_{{ loc.id }}" value="{{ loc.id }}" {% if selected_location_id == loc.id|stringformat:"s" %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-accent btn-sm" for="wl_{{ loc.id }}">{{ loc }}</label>
</div>
{% endfor %}
</div>
</div>
<div class="col-md-4">
<div class="small text-muted mb-1 fw-bold">Тип:</div>
<div class="d-flex flex-wrap gap-1">
<div>
<input type="radio" class="btn-check" name="kind" id="wk_all" value="" {% if not selected_kind %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-accent btn-sm" for="wk_all">Все</label>
</div>
<div>
<input type="radio" class="btn-check" name="kind" id="wk_raw" value="raw" {% if selected_kind == 'raw' %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-primary btn-sm" for="wk_raw">Сырьё</label>
</div>
<div>
<input type="radio" class="btn-check" name="kind" id="wk_finished" value="finished" {% if selected_kind == 'finished' %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-success btn-sm" for="wk_finished">Изделия</label>
</div>
</div>
</div>
<div class="col-md-auto">
<div class="small text-muted mb-1 fw-bold">Период:</div>
<div class="d-flex gap-2">
<input type="date" name="start_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ start_date }}" onchange="this.form.submit()">
<input type="date" name="end_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ end_date }}" onchange="this.form.submit()">
</div>
</div>
<div class="col-md-1 text-end mt-auto">
<a href="{% url 'warehouse_stocks' %}?reset=1" class="btn btn-outline-secondary btn-sm w-100" title="Сброс">
<i class="bi bi-arrow-counterclockwise"></i>
</a>
</div>
</form>
</div>
</div>
<div class="card shadow border-secondary">
<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-box-seam me-2"></i>Склады</h3>
<div class="d-flex flex-wrap gap-2 align-items-center">
{% if can_receive %}
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#receiptModal">
<i class="bi bi-box-arrow-in-down me-1"></i>Приход
</button>
{% endif %}
<form class="d-flex gap-2 align-items-center" method="get" action="{% url 'warehouse_stocks' %}">
<input type="hidden" name="location_id" value="{{ selected_location_id }}">
<input type="hidden" name="kind" value="{{ selected_kind }}">
<input type="hidden" name="start_date" value="{{ start_date }}">
<input type="hidden" name="end_date" value="{{ end_date }}">
<input class="form-control form-control-sm" name="q" value="{{ q }}" placeholder="Поиск (материал, деталь, склад, ID)" style="min-width: 360px;">
<button class="btn btn-outline-secondary btn-sm" type="submit"><i class="bi bi-search me-1"></i>Найти</button>
</form>
</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-type="date">Поступление</th>
<th>Сделка</th>
<th>Наименование</th>
<th>Тип</th>
<th data-sort-type="number">Кол-во</th>
<th>Ед. измерения</th>
<th>ДО</th>
<th data-sort="false">Действия</th>
</tr>
</thead>
<tbody>
{% for it in items %}
<tr>
<td>{{ it.location }}</td>
<td>{% if it.created_at %}{{ it.created_at|date:"d.m.Y H:i" }}{% endif %}</td>
<td>
{% if it.deal_id %}
<span class="text-accent fw-bold">{{ it.deal.number }}</span>
{% else %}
{% endif %}
</td>
<td>
{% if it.material_id %}
{{ it.material.full_name }}
{% elif it.entity_id %}
{{ it.entity }}
{% else %}
{% endif %}
{% if it.unique_id %}
<div class="small text-muted mt-1">{{ it.unique_id }}</div>
{% endif %}
</td>
<td>
{% if it.entity_id %}
Изделие/деталь
{% elif it.is_remnant %}
ДО
{% else %}
Сырьё
{% endif %}
</td>
<td>{{ it.quantity }}</td>
<td>
{% if it.entity_id %}
шт
{% elif it.material_id and it.material.category_id %}
{% with ff=it.material.category.form_factor|stringformat:"s"|lower %}
{% if ff == 'лист' or ff == 'sheet' %}лист
{% elif ff == 'прокат' or ff == 'rolled' or ff == 'roll' %}прокат
{% else %}ед.
{% endif %}
{% endwith %}
{% else %}
ед.
{% endif %}
</td>
<td>
{% if it.is_remnant %}Да{% else %}—{% endif %}
</td>
<td>
{% if can_transfer %}
<div class="d-flex gap-2">
<button
type="button"
class="btn btn-outline-accent btn-sm"
data-bs-toggle="modal"
data-bs-target="#transferModal"
data-mode="transfer"
data-stock-item-id="{{ it.id }}"
data-stock-item-name="{% if it.material_id %}{{ it.material.full_name }}{% elif it.entity_id %}{{ it.entity }}{% else %}—{% endif %}"
data-from-location="{{ it.location }}"
data-from-location-id="{{ it.location_id }}"
data-max="{{ it.quantity }}"
>
<i class="bi bi-arrow-left-right me-1"></i>Переместить
</button>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal"
data-bs-target="#transferModal"
data-mode="ship"
data-stock-item-id="{{ it.id }}"
data-stock-item-name="{% if it.material_id %}{{ it.material.full_name }}{% elif it.entity_id %}{{ it.entity }}{% else %}—{% endif %}"
data-from-location="{{ it.location }}"
data-from-location-id="{{ it.location_id }}"
data-max="{{ it.quantity }}"
>
<i class="bi bi-truck me-1"></i>Отгрузка
</button>
</div>
{% else %}
<span class="text-muted small">только просмотр</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center text-muted py-4">Нет позиций по текущим фильтрам</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="receiptModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form method="post" action="{% url 'warehouse_receipt' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<div class="modal-header border-secondary">
<h5 class="modal-title">Приход</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Тип</label>
<select class="form-select" name="kind" id="receiptKind" required>
<option value="raw">Сырьё / покупное</option>
<option value="entity">Изделие/деталь</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Сделка</label>
<select class="form-select" name="deal_id">
<option value="">— не указано —</option>
{% for d in deals %}
<option value="{{ d.id }}">{{ d.number }}{% if d.company_id %} — {{ d.company.name }}{% endif %}{% if d.description %} — {{ d.description }}{% endif %}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Склад</label>
<select class="form-select" name="location_id" required>
{% for loc in locations %}
<option value="{{ loc.id }}">{{ loc }}</option>
{% endfor %}
</select>
</div>
<div class="col-12" id="receiptRawBlock">
<label class="form-label">Материал</label>
<select class="form-select" name="material_id" id="receiptMaterial">
{% for m in materials %}
<option value="{{ m.id }}" data-ff="{{ m.category.form_factor|default:'' }}">{{ m.full_name|default:m.name }}</option>
{% endfor %}
</select>
<div class="row g-2 mt-1">
<div class="col-md-4">
<label class="form-label">Кол-во</label>
<input class="form-control" name="quantity" id="receiptQtyRaw" placeholder="Напр. 1" required>
</div>
<div class="col-md-4">
<label class="form-label">Длина (мм)</label>
<input class="form-control" name="current_length" id="receiptLen" placeholder="Напр. 2500">
</div>
<div class="col-md-4">
<label class="form-label">Ширина (мм)</label>
<input class="form-control" name="current_width" id="receiptWid" placeholder="Напр. 1250">
</div>
<div class="col-12 mt-1">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_customer_supplied" id="receiptDav">
<label class="form-check-label" for="receiptDav">Давальческий</label>
</div>
</div>
</div>
</div>
<div class="col-12" id="receiptEntityBlock" style="display:none;">
<div class="row g-2">
<div class="col-md-8">
<label class="form-label">КД (изделие/деталь)</label>
<select class="form-select" name="entity_id">
{% for e in entities %}
<option value="{{ e.id }}">{{ e }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Кол-во</label>
<input class="form-control" name="quantity" id="receiptQtyEntity" placeholder="Напр. 1" required>
</div>
</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">Добавить</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="transferModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<form method="post" action="{% url 'warehouse_transfer' %}" class="modal-content border-secondary">
{% csrf_token %}
<input type="hidden" name="stock_item_id" id="transferStockItemId">
<input type="hidden" name="next" value="{{ request.get_full_path }}">
<div class="modal-header border-secondary">
<h5 class="modal-title" id="transferTitle">Перемещение</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<div class="fw-bold" id="transferInfoFrom"></div>
<div class="small text-muted" id="transferInfoName"></div>
<div class="small text-muted" id="transferInfoAvail"></div>
</div>
<div class="alert alert-danger d-none" id="transferError" role="alert"></div>
<div class="row g-2">
<div class="col-md-6" id="transferToCol">
<label class="form-label">Куда</label>
<select class="form-select" name="to_location_id" id="transferToLocation" required>
{% if shipping_location_id %}
<option value="{{ shipping_location_id }}" data-shipping="1" hidden disabled>{{ shipping_location_label|default:'Отгруженные позиции' }}</option>
{% endif %}
{% for loc in locations %}
<option value="{{ loc.id }}">{{ loc }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Количество</label>
<input class="form-control" name="quantity" id="transferQty" placeholder="Напр. 1 или 2.5" inputmode="decimal" autofocus required>
<div class="small text-muted mt-1" id="transferMaxHint"></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">Применить</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('transferModal');
if (!modal) return;
const form = modal.querySelector('form');
const idInput = document.getElementById('transferStockItemId');
const title = document.getElementById('transferTitle');
const infoFrom = document.getElementById('transferInfoFrom');
const infoName = document.getElementById('transferInfoName');
const infoAvail = document.getElementById('transferInfoAvail');
const errBox = document.getElementById('transferError');
const qty = document.getElementById('transferQty');
const maxHint = document.getElementById('transferMaxHint');
const toSel = document.getElementById('transferToLocation');
const toCol = document.getElementById('transferToCol');
const receiptKind = document.getElementById('receiptKind');
const receiptRaw = document.getElementById('receiptRawBlock');
const receiptEntity = document.getElementById('receiptEntityBlock');
const receiptMaterial = document.getElementById('receiptMaterial');
const receiptLen = document.getElementById('receiptLen');
const receiptWid = document.getElementById('receiptWid');
const receiptQtyRaw = document.getElementById('receiptQtyRaw');
const receiptQtyEntity = document.getElementById('receiptQtyEntity');
let currentMax = null;
let currentMode = 'transfer';
function showErr(text) {
if (!errBox) return;
if (!text) {
errBox.classList.add('d-none');
errBox.textContent = '';
return;
}
errBox.textContent = text;
errBox.classList.remove('d-none');
}
function parseNumber(text) {
const s = (text || '').toString().trim().replace(',', '.');
const n = parseFloat(s);
return isNaN(n) ? null : n;
}
modal.addEventListener('show.bs.modal', (ev) => {
const btn = ev.relatedTarget;
if (!btn) return;
currentMode = btn.getAttribute('data-mode') || 'transfer';
const stockItemId = btn.getAttribute('data-stock-item-id') || '';
const name = btn.getAttribute('data-stock-item-name') || '';
const fromLoc = btn.getAttribute('data-from-location') || '';
const fromLocId = btn.getAttribute('data-from-location-id') || '';
const maxRaw = btn.getAttribute('data-max') || '';
currentMax = parseNumber(maxRaw);
showErr('');
if (idInput) idInput.value = stockItemId;
if (title) title.textContent = currentMode === 'ship' ? 'Отгрузка' : 'Перемещение';
if (infoFrom) infoFrom.textContent = `Откуда: ${fromLoc}`;
if (infoName) infoName.textContent = `Что: ${name}`;
if (infoAvail) infoAvail.textContent = currentMax !== null ? `Доступно: ${currentMax}` : '';
if (maxHint) maxHint.textContent = currentMax !== null ? `Доступно: ${currentMax}` : '';
if (qty) {
qty.value = currentMax !== null ? String(currentMax) : '';
if (currentMax !== null) qty.setAttribute('max', String(currentMax));
qty.setAttribute('min', '0');
qty.setAttribute('step', 'any');
}
if (toSel) {
const shipId = '{{ shipping_location_id }}';
Array.from(toSel.options).forEach(opt => {
const isShipping = opt.getAttribute('data-shipping') === '1';
if (isShipping) {
if (currentMode === 'ship') {
opt.disabled = false;
opt.hidden = false;
} else {
opt.disabled = true;
opt.hidden = true;
}
return;
}
if (fromLocId && String(opt.value) === String(fromLocId)) {
opt.disabled = true;
opt.hidden = true;
} else {
opt.disabled = false;
opt.hidden = false;
}
});
const col = toCol || (toSel ? toSel.closest('.col-md-6') : null);
if (currentMode === 'ship') {
if (shipId) {
toSel.value = shipId;
}
if (col) col.style.display = 'none';
} else {
if (col) col.style.display = '';
const first = Array.from(toSel.options).find(o => !o.disabled && !o.hidden);
if (first) toSel.value = first.value;
}
}
});
modal.addEventListener('shown.bs.modal', () => {
if (qty) {
qty.focus();
qty.select();
}
});
if (form) {
form.addEventListener('submit', (e) => {
const v = parseNumber(qty ? qty.value : '');
if (v === null || v <= 0) {
e.preventDefault();
showErr('Количество должно быть больше 0.');
if (qty) {
qty.focus();
qty.select();
}
return;
}
if (currentMax !== null && v > currentMax) {
e.preventDefault();
showErr('Нельзя переместить больше, чем доступно.');
if (qty) {
qty.focus();
qty.select();
}
return;
}
if (currentMode === 'ship') {
const shipId = '{{ shipping_location_id }}';
if (!shipId) {
e.preventDefault();
showErr('Не найден склад отгруженных позиций. Создай склад с названием содержащим "отгруж" или "отгруз".');
return;
}
}
showErr('');
});
}
function syncReceiptKind() {
if (!receiptKind || !receiptRaw || !receiptEntity) return;
const isRaw = (receiptKind.value || '') === 'raw';
receiptRaw.style.display = isRaw ? '' : 'none';
receiptEntity.style.display = isRaw ? 'none' : '';
if (receiptQtyRaw) {
receiptQtyRaw.disabled = !isRaw;
receiptQtyRaw.required = isRaw;
}
if (receiptQtyEntity) {
receiptQtyEntity.disabled = isRaw;
receiptQtyEntity.required = !isRaw;
}
}
function applyReceiptDefaults() {
if (!receiptMaterial) return;
const opt = receiptMaterial.options[receiptMaterial.selectedIndex];
const ff = (opt && opt.getAttribute('data-ff') || '').toLowerCase();
if (ff === 'sheet') {
if (receiptLen && !receiptLen.value) receiptLen.value = '2500';
if (receiptWid && !receiptWid.value) receiptWid.value = '1250';
if (receiptWid) receiptWid.disabled = false;
} else if (ff === 'bar') {
if (receiptLen && !receiptLen.value) receiptLen.value = '6000';
if (receiptWid) {
receiptWid.value = '';
receiptWid.disabled = true;
}
} else {
if (receiptWid) receiptWid.disabled = false;
}
}
if (receiptKind) {
receiptKind.addEventListener('change', () => {
syncReceiptKind();
if (receiptKind.value === 'raw') {
if (receiptQtyRaw) {
receiptQtyRaw.focus();
receiptQtyRaw.select();
}
} else {
if (receiptQtyEntity) {
receiptQtyEntity.focus();
receiptQtyEntity.select();
}
}
});
syncReceiptKind();
}
if (receiptMaterial) {
receiptMaterial.addEventListener('change', applyReceiptDefaults);
applyReceiptDefaults();
}
});
</script>
{% endblock %}