This commit is contained in:
270
shiftflow/templates/shiftflow/closing.html
Normal file
270
shiftflow/templates/shiftflow/closing.html
Normal file
@@ -0,0 +1,270 @@
|
||||
{% 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 '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">
|
||||
<h3 class="text-accent mb-0"><i class="bi bi-check2-square me-2"></i>Закрытие</h3>
|
||||
</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 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>{{ s }}</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="4" 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 %}
|
||||
@@ -134,39 +134,11 @@
|
||||
<input type="number" name="quantity_fact" id="id_quantity_fact" class="form-control border-secondary" value="{{ item.quantity_fact }}" max="{{ item.quantity_plan }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Взятый материал</label>
|
||||
<input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Остаток ДО</label>
|
||||
<input type="text" name="usable_waste" class="form-control border-secondary" value="{{ item.usable_waste }}" placeholder="Напр: 0.8м / 12кг" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Лом (кг)</label>
|
||||
<input type="number" step="0.01" min="0" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight|default_if_none:'0'|unlocalize }}" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div>
|
||||
<input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">
|
||||
{% if user_role == 'master' %}
|
||||
<div class="row g-3 mt-3 text-start">
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Взятый материал</label>
|
||||
<input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Остаток ДО</label>
|
||||
<input type="text" name="usable_waste" class="form-control border-secondary" value="{{ item.usable_waste }}" placeholder="Напр: 0.8м / 12кг">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Лом (кг)</label>
|
||||
<input type="number" step="0.01" min="0" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight|default_if_none:'0'|unlocalize }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -202,18 +174,6 @@
|
||||
<input type="number" name="quantity_fact" class="form-control border-secondary" value="{{ item.quantity_fact }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Взятый материал</label>
|
||||
<input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Деловой отход</label>
|
||||
<input type="text" name="usable_waste" class="form-control border-secondary" value="{{ item.usable_waste }}" placeholder="Напр: кусок 1500мм">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="small text-muted">Лом (кг)</label>
|
||||
<input type="number" step="0.01" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight|default_if_none:'0'|unlocalize }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch p-3 rounded border border-warning mb-4 bg-body-tertiary d-flex justify-content-between align-items-center">
|
||||
@@ -223,28 +183,13 @@
|
||||
{% endif %}
|
||||
|
||||
{% if user_role == 'clerk' %}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">Взятый материал</small>
|
||||
<strong>{{ item.material_taken|default:"-" }}</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">Остаток ДО</small>
|
||||
<strong>{{ item.usable_waste|default:"-" }}</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">Лом (кг)</small>
|
||||
<strong>{{ item.scrap_weight }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if item.status == 'done' or item.status == 'partial' %}
|
||||
<div class="form-check form-switch p-3 rounded border border-warning mb-4 bg-body-tertiary d-flex justify-content-between align-items-center">
|
||||
<label class="form-check-label fw-bold ms-2" for="sync1c">Списано в 1С</label>
|
||||
<input class="form-check-input ms-0" style="width: 3em; height: 1.5em;" type="checkbox" name="is_synced_1c" id="sync1c" {% if item.is_synced_1c %}checked{% endif %}>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted small mb-4"><i class="bi bi-info-circle me-1"></i>Списание будет доступно после закрытия (Выполнено/Частично).</div>
|
||||
<div class="text-muted small mb-4"><i class="bi bi-info-circle me-1"></i>Списание будет доступно после закрытия.</div>
|
||||
{% endif %}
|
||||
<input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">
|
||||
{% endif %}
|
||||
@@ -253,7 +198,7 @@
|
||||
<a href="{{ back_url }}" class="btn btn-outline-secondary">Назад</a>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="hidden" name="action" id="actionField" value="save">
|
||||
{% if item.status == 'work' %}
|
||||
{% if item.status == 'work' and user_role == 'admin' %}
|
||||
<button type="submit" class="btn btn-success px-4" onclick="document.getElementById('actionField').value='close_done'">
|
||||
<i class="bi bi-check-all me-2"></i>Выполнено
|
||||
</button>
|
||||
|
||||
560
shiftflow/templates/shiftflow/warehouse_stocks.html
Normal file
560
shiftflow/templates/shiftflow/warehouse_stocks.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user