Создали планирование
All checks were successful
Deploy MES Core / deploy (push) Successful in 12s

This commit is contained in:
2026-03-30 01:39:22 +03:00
parent 78d4a1a04f
commit c9ff66a36b
6 changed files with 1053 additions and 5 deletions

29
shiftflow/forms.py Normal file
View File

@@ -0,0 +1,29 @@
from django import forms
from warehouse.models import Material
from .models import Deal
class ProductionTaskCreateForm(forms.Form):
drawing_name = forms.CharField(label="Наименование детали", max_length=255, required=False)
quantity_ordered = forms.IntegerField(label="Требуется (шт)", min_value=1)
size_value = forms.FloatField(label="Размер (мм)", min_value=0)
is_bend = forms.BooleanField(label="Гибка", required=False)
drawing_file = forms.FileField(label="Исходник (DXF/IGES)", required=False)
extra_drawing = forms.FileField(label="Доп. чертеж (PDF)", required=False)
deal = forms.ModelChoiceField(
label="Сделка",
queryset=Deal.objects.all().order_by("number"),
required=True,
empty_label="— выбрать —",
)
material = forms.ModelChoiceField(
label="Материал",
queryset=Material.objects.all().order_by("full_name"),
required=True,
empty_label="— выбрать —",
)

View File

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

View File

@@ -0,0 +1,584 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<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-plus-circle me-2"></i>Новое задание</h3>
<a class="btn btn-outline-secondary btn-sm" href="{% url 'planning' %}">
<i class="bi bi-arrow-left me-1"></i>Назад
</a>
</div>
<form method="post" enctype="multipart/form-data" class="card-body p-4">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{% for e in form.non_field_errors %}<div>{{ e }}</div>{% endfor %}
</div>
{% endif %}
<div class="row g-3">
<div class="col-12">
<div class="card border-secondary">
<div class="card-header border-secondary py-2">
<strong>Деталь</strong>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-8">
<label class="form-label small text-muted">{{ form.drawing_name.label }}</label>
{{ form.drawing_name }}
{% for e in form.drawing_name.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-4">
<label class="form-label small text-muted">{{ form.quantity_ordered.label }}</label>
{{ form.quantity_ordered }}
{% for e in form.quantity_ordered.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-4">
<label class="form-label small text-muted">{{ form.size_value.label }}</label>
{{ form.size_value }}
{% for e in form.size_value.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-8 d-flex align-items-end">
<div class="form-check form-switch">
{{ form.is_bend }}
<label class="form-check-label ms-2">{{ form.is_bend.label }}</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-secondary">
<div class="card-header border-secondary py-2">
<strong>Файлы</strong>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.drawing_file.label }}</label>
{{ form.drawing_file }}
<button type="button" id="fillNameFromSource" class="btn btn-outline-secondary btn-sm mt-2" disabled>
в название (из исходника)
</button>
</div>
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.extra_drawing.label }}</label>
{{ form.extra_drawing }}
<button type="button" id="fillNameFromPdf" class="btn btn-outline-secondary btn-sm mt-2" disabled>
в название (из чертежа)
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-secondary">
<div class="card-header border-secondary py-2">
<strong>Сделка и материал</strong>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.deal.label }}</label>
<div class="d-flex gap-2">
<div class="flex-grow-1">{{ form.deal }}</div>
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="create">Создать</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="dealEditBtn" data-bs-toggle="modal" data-bs-target="#dealModal" data-mode="edit" disabled>Редакт.</button>
</div>
{% for e in form.deal.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
<div class="col-md-6">
<label class="form-label small text-muted">{{ form.material.label }}</label>
<div class="d-flex gap-2">
<div class="flex-grow-1">{{ form.material }}</div>
<button type="button" class="btn btn-outline-accent btn-sm" data-bs-toggle="modal" data-bs-target="#materialModal" data-mode="create">Создать</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="materialEditBtn" data-bs-toggle="modal" data-bs-target="#materialModal" data-mode="edit" disabled>Редакт.</button>
</div>
{% for e in form.material.errors %}<div class="text-danger small">{{ e }}</div>{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-outline-accent px-4 fw-bold">
<i class="bi bi-save me-2"></i>Создать
</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="dealModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Сделка</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="dealId">
<div class="mb-3">
<label class="form-label small text-muted">№ Сделки</label>
<input type="text" class="form-control border-secondary" id="dealNumber">
</div>
<div class="mb-3">
<label class="form-label small text-muted">Компания</label>
<div class="d-flex gap-2">
<select class="form-select border-secondary" id="dealCompany">
<option value="">— не выбрано —</option>
{% for c in companies %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-accent btn-sm" id="openCompanyModalBtn">
Создать
</button>
</div>
</div>
<div class="mb-0">
<label class="form-label small text-muted">Описание</label>
<textarea class="form-control border-secondary" rows="3" id="dealDescription"></textarea>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="dealSaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="materialModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Материал</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="materialId">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small text-muted">Категория</label>
<div class="d-flex gap-2">
<select class="form-select border-secondary" id="materialCategory">
<option value="">— выбрать —</option>
{% for c in material_categories %}
<option value="{{ c.id }}">{{ c.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-accent btn-sm" id="openCategoryModalBtn">
Создать
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label small text-muted">Марка стали</label>
<div class="d-flex gap-2">
<select class="form-select border-secondary" id="materialGrade">
<option value="">— не выбрано —</option>
{% for g in steel_grades %}
<option value="{{ g.id }}">{{ g.name }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-outline-accent btn-sm" id="openGradeModalBtn">
Создать
</button>
</div>
</div>
<div class="col-12">
<label class="form-label small text-muted">Наименование (размер/характеристики)</label>
<input type="text" class="form-control border-secondary" id="materialName">
</div>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="materialSaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="companyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Компания</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="companyId">
<div class="mb-3">
<label class="form-label small text-muted">Название</label>
<input type="text" class="form-control border-secondary" id="companyName">
</div>
<div class="mb-0">
<label class="form-label small text-muted">Примечание</label>
<textarea class="form-control border-secondary" rows="3" id="companyDescription"></textarea>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="companySaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Категория материала</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="categoryId">
<div class="mb-3">
<label class="form-label small text-muted">Название</label>
<input type="text" class="form-control border-secondary" id="categoryName">
</div>
<div class="mb-0">
<label class="form-label small text-muted">ГОСТ</label>
<input type="text" class="form-control border-secondary" id="categoryGost">
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="categorySaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="gradeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title">Марка стали</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body">
<input type="hidden" id="gradeId">
<div class="mb-3">
<label class="form-label small text-muted">Марка стали</label>
<input type="text" class="form-control border-secondary" id="gradeName">
</div>
<div class="mb-0">
<label class="form-label small text-muted">ГОСТ/ТУ</label>
<input type="text" class="form-control border-secondary" id="gradeGost">
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-outline-accent" id="gradeSaveBtn">Сохранить</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const drawingFile = document.getElementById('id_drawing_file');
const extraDrawing = document.getElementById('id_extra_drawing');
const drawingName = document.getElementById('id_drawing_name');
const fillFromSource = document.getElementById('fillNameFromSource');
const fillFromPdf = document.getElementById('fillNameFromPdf');
const dealSelect = document.getElementById('id_deal');
const materialSelect = document.getElementById('id_material');
const dealEditBtn = document.getElementById('dealEditBtn');
const materialEditBtn = document.getElementById('materialEditBtn');
const dealModal = document.getElementById('dealModal');
const dealId = document.getElementById('dealId');
const dealNumber = document.getElementById('dealNumber');
const dealCompany = document.getElementById('dealCompany');
const dealDescription = document.getElementById('dealDescription');
const dealSaveBtn = document.getElementById('dealSaveBtn');
const companyModal = document.getElementById('companyModal');
const companyId = document.getElementById('companyId');
const companyName = document.getElementById('companyName');
const companyDescription = document.getElementById('companyDescription');
const companySaveBtn = document.getElementById('companySaveBtn');
const openCompanyModalBtn = document.getElementById('openCompanyModalBtn');
const materialModal = document.getElementById('materialModal');
const materialId = document.getElementById('materialId');
const materialCategory = document.getElementById('materialCategory');
const materialGrade = document.getElementById('materialGrade');
const materialName = document.getElementById('materialName');
const materialSaveBtn = document.getElementById('materialSaveBtn');
const categoryModal = document.getElementById('categoryModal');
const categoryId = document.getElementById('categoryId');
const categoryName = document.getElementById('categoryName');
const categoryGost = document.getElementById('categoryGost');
const categorySaveBtn = document.getElementById('categorySaveBtn');
const openCategoryModalBtn = document.getElementById('openCategoryModalBtn');
const gradeModal = document.getElementById('gradeModal');
const gradeId = document.getElementById('gradeId');
const gradeName = document.getElementById('gradeName');
const gradeGost = document.getElementById('gradeGost');
const gradeSaveBtn = document.getElementById('gradeSaveBtn');
const openGradeModalBtn = document.getElementById('openGradeModalBtn');
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return '';
}
function filenameBase(path) {
if (!path) return '';
const justName = path.split('\\').pop().split('/').pop();
const idx = justName.lastIndexOf('.');
return idx > 0 ? justName.slice(0, idx) : justName;
}
function updateFileButtons() {
const hasSource = drawingFile && drawingFile.files && drawingFile.files.length > 0;
const hasPdf = extraDrawing && extraDrawing.files && extraDrawing.files.length > 0;
if (fillFromSource) fillFromSource.disabled = !hasSource;
if (fillFromPdf) fillFromPdf.disabled = !hasPdf;
}
if (drawingFile) drawingFile.addEventListener('change', updateFileButtons);
if (extraDrawing) extraDrawing.addEventListener('change', updateFileButtons);
updateFileButtons();
if (fillFromSource) {
fillFromSource.addEventListener('click', function () {
if (!drawingFile || !drawingFile.files || drawingFile.files.length === 0) return;
const base = filenameBase(drawingFile.files[0].name);
if (drawingName) drawingName.value = base;
});
}
if (fillFromPdf) {
fillFromPdf.addEventListener('click', function () {
if (!extraDrawing || !extraDrawing.files || extraDrawing.files.length === 0) return;
const base = filenameBase(extraDrawing.files[0].name);
if (drawingName) drawingName.value = base;
});
}
function updateEditButtons() {
if (dealEditBtn) dealEditBtn.disabled = !(dealSelect && dealSelect.value);
if (materialEditBtn) materialEditBtn.disabled = !(materialSelect && materialSelect.value);
}
function openChildModal(parentEl, childEl) {
if (!parentEl || !childEl) return;
childEl.dataset.returnTo = parentEl.id;
const parent = bootstrap.Modal.getOrCreateInstance(parentEl);
const child = bootstrap.Modal.getOrCreateInstance(childEl);
parentEl.addEventListener('hidden.bs.modal', function () {
child.show();
}, { once: true });
parent.hide();
}
function returnToParent(childEl) {
const returnId = childEl ? childEl.dataset.returnTo : '';
if (!returnId) return;
delete childEl.dataset.returnTo;
const parentEl = document.getElementById(returnId);
if (parentEl) bootstrap.Modal.getOrCreateInstance(parentEl).show();
}
if (companyModal) companyModal.addEventListener('hidden.bs.modal', function () { returnToParent(companyModal); });
if (categoryModal) categoryModal.addEventListener('hidden.bs.modal', function () { returnToParent(categoryModal); });
if (gradeModal) gradeModal.addEventListener('hidden.bs.modal', function () { returnToParent(gradeModal); });
if (openCompanyModalBtn) openCompanyModalBtn.addEventListener('click', function () { openChildModal(dealModal, companyModal); });
if (openCategoryModalBtn) openCategoryModalBtn.addEventListener('click', function () { openChildModal(materialModal, categoryModal); });
if (openGradeModalBtn) openGradeModalBtn.addEventListener('click', function () { openChildModal(materialModal, gradeModal); });
if (dealSelect) dealSelect.addEventListener('change', updateEditButtons);
if (materialSelect) materialSelect.addEventListener('change', updateEditButtons);
updateEditButtons();
async function postForm(url, data) {
const csrftoken = getCookie('csrftoken');
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'X-CSRFToken': csrftoken,
},
body: new URLSearchParams(data).toString(),
});
if (!res.ok) throw new Error('request_failed');
return await res.json();
}
async function getJson(url) {
const res = await fetch(url, { method: 'GET' });
if (!res.ok) throw new Error('request_failed');
return await res.json();
}
function upsertSelectOption(select, id, label) {
if (!select) return;
let opt = select.querySelector(`option[value="${id}"]`);
if (!opt) {
opt = document.createElement('option');
opt.value = String(id);
select.appendChild(opt);
}
opt.textContent = label;
select.value = String(id);
select.dispatchEvent(new Event('change'));
}
if (dealModal) {
dealModal.addEventListener('show.bs.modal', async function (event) {
if (!event.relatedTarget) return;
const mode = event.relatedTarget.getAttribute('data-mode') || 'create';
dealId.value = '';
dealNumber.value = '';
dealCompany.value = '';
dealDescription.value = '';
if (mode === 'edit' && dealSelect && dealSelect.value) {
const data = await getJson(`/planning/deal/${dealSelect.value}/json/`);
dealId.value = data.id;
dealNumber.value = data.number || '';
dealCompany.value = data.company_id ? String(data.company_id) : '';
dealDescription.value = data.description || '';
}
});
}
if (dealSaveBtn) {
dealSaveBtn.addEventListener('click', async function () {
const payload = {
id: dealId.value,
number: dealNumber.value,
company_id: dealCompany.value,
description: dealDescription.value,
};
const data = await postForm('{% url "deal_upsert" %}', payload);
upsertSelectOption(dealSelect, data.id, data.label);
bootstrap.Modal.getInstance(dealModal).hide();
});
}
if (companyModal) {
companyModal.addEventListener('show.bs.modal', function () {
companyId.value = '';
companyName.value = '';
companyDescription.value = '';
});
}
if (companySaveBtn) {
companySaveBtn.addEventListener('click', async function () {
const payload = {
id: companyId.value,
name: companyName.value,
description: companyDescription.value,
};
const data = await postForm('{% url "company_upsert" %}', payload);
upsertSelectOption(dealCompany, data.id, data.label);
bootstrap.Modal.getInstance(companyModal).hide();
});
}
if (categoryModal) {
categoryModal.addEventListener('show.bs.modal', function () {
categoryId.value = '';
categoryName.value = '';
categoryGost.value = '';
});
}
if (categorySaveBtn) {
categorySaveBtn.addEventListener('click', async function () {
const payload = {
id: categoryId.value,
name: categoryName.value,
gost_standard: categoryGost.value,
};
const data = await postForm('{% url "material_category_upsert" %}', payload);
upsertSelectOption(materialCategory, data.id, data.label);
bootstrap.Modal.getInstance(categoryModal).hide();
});
}
if (gradeModal) {
gradeModal.addEventListener('show.bs.modal', function () {
gradeId.value = '';
gradeName.value = '';
gradeGost.value = '';
});
}
if (gradeSaveBtn) {
gradeSaveBtn.addEventListener('click', async function () {
const payload = {
id: gradeId.value,
name: gradeName.value,
gost_standard: gradeGost.value,
};
const data = await postForm('{% url "steel_grade_upsert" %}', payload);
upsertSelectOption(materialGrade, data.id, data.label);
bootstrap.Modal.getInstance(gradeModal).hide();
});
}
if (materialModal) {
materialModal.addEventListener('show.bs.modal', async function (event) {
if (!event.relatedTarget) return;
const mode = event.relatedTarget.getAttribute('data-mode') || 'create';
materialId.value = '';
materialCategory.value = '';
materialGrade.value = '';
materialName.value = '';
if (mode === 'edit' && materialSelect && materialSelect.value) {
const data = await getJson(`/planning/material/${materialSelect.value}/json/`);
materialId.value = data.id;
materialCategory.value = data.category_id ? String(data.category_id) : '';
materialGrade.value = data.steel_grade_id ? String(data.steel_grade_id) : '';
materialName.value = data.name || '';
}
});
}
if (materialSaveBtn) {
materialSaveBtn.addEventListener('click', async function () {
const payload = {
id: materialId.value,
category_id: materialCategory.value,
steel_grade_id: materialGrade.value,
name: materialName.value,
};
const data = await postForm('{% url "material_upsert" %}', payload);
upsertSelectOption(materialSelect, data.id, data.label);
bootstrap.Modal.getInstance(materialModal).hide();
});
}
});
</script>
{% endblock %}

View File

@@ -1,9 +1,19 @@
from django.urls import path from django.urls import path
from .views import ( from .views import (
CompanyUpsertView,
DealDetailView,
DealUpsertView,
IndexView, IndexView,
ItemUpdateView, ItemUpdateView,
RegistryView, MaterialCategoryUpsertView,
MaterialDetailView,
MaterialUpsertView,
PlanningAddView,
PlanningView,
ProductionTaskCreateView,
RegistryPrintView, RegistryPrintView,
RegistryView,
SteelGradeUpsertView,
) )
urlpatterns = [ urlpatterns = [
@@ -12,6 +22,17 @@ urlpatterns = [
# Реестр # Реестр
path('registry/', RegistryView.as_view(), name='registry'), path('registry/', RegistryView.as_view(), name='registry'),
# Планирование
path('planning/', PlanningView.as_view(), name='planning'),
path('planning/add/', PlanningAddView.as_view(), name='planning_add'),
path('planning/task/add/', ProductionTaskCreateView.as_view(), name='task_add'),
path('planning/deal/<int:pk>/json/', DealDetailView.as_view(), name='deal_json'),
path('planning/deal/upsert/', DealUpsertView.as_view(), name='deal_upsert'),
path('planning/company/upsert/', CompanyUpsertView.as_view(), name='company_upsert'),
path('planning/material/<int:pk>/json/', MaterialDetailView.as_view(), name='material_json'),
path('planning/material/upsert/', MaterialUpsertView.as_view(), name='material_upsert'),
path('planning/material-category/upsert/', MaterialCategoryUpsertView.as_view(), name='material_category_upsert'),
path('planning/steel-grade/upsert/', SteelGradeUpsertView.as_view(), name='steel_grade_upsert'),
# Печать сменного листа # Печать сменного листа
path('registry/print/', RegistryPrintView.as_view(), name='registry_print'), path('registry/print/', RegistryPrintView.as_view(), name='registry_print'),
path('item/<int:pk>/', ItemUpdateView.as_view(), name='item_detail'), path('item/<int:pk>/', ItemUpdateView.as_view(), name='item_detail'),

View File

@@ -1,11 +1,19 @@
from datetime import datetime from datetime import datetime
from django.shortcuts import redirect from django.db.models import Case, ExpressionWrapper, F, IntegerField, Sum, Value, When
from django.db.models.functions import Coalesce
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic import TemplateView, ListView, UpdateView from django.views import View
from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils import timezone from django.utils import timezone
from .models import Item, Machine
from warehouse.models import Material, MaterialCategory, SteelGrade
from .forms import ProductionTaskCreateForm
from .models import Company, Deal, Item, Machine, ProductionTask
# Класс главной страницы (роутер) # Класс главной страницы (роутер)
class IndexView(TemplateView): class IndexView(TemplateView):
@@ -208,6 +216,288 @@ class RegistryPrintView(LoginRequiredMixin, TemplateView):
return context return context
class PlanningView(LoginRequiredMixin, TemplateView):
template_name = 'shiftflow/planning.html'
def dispatch(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return redirect('registry')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
profile = getattr(self.request.user, 'profile', None)
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
context['user_role'] = role
tasks = ProductionTask.objects.select_related('deal', 'material').annotate(
done_qty=Coalesce(Sum('items__quantity_fact'), 0),
planned_qty=Coalesce(
Sum(
Case(
When(items__status__in=['work', 'leftover'], then=F('items__quantity_plan')),
default=Value(0),
output_field=IntegerField(),
)
),
0,
),
).annotate(
remaining_qty=ExpressionWrapper(
F('quantity_ordered') - F('done_qty') - F('planned_qty'),
output_field=IntegerField(),
)
)
context['tasks'] = tasks
context['machines'] = Machine.objects.all()
return context
class PlanningAddView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return redirect('planning')
task_id = request.POST.get('task_id')
machine_id = request.POST.get('machine_id')
qty_raw = request.POST.get('quantity_plan')
if not (task_id and task_id.isdigit() and machine_id and machine_id.isdigit() and qty_raw and qty_raw.isdigit()):
return redirect('planning')
qty = int(qty_raw)
if qty <= 0:
return redirect('planning')
Item.objects.create(
task_id=int(task_id),
machine_id=int(machine_id),
date=timezone.localdate(),
quantity_plan=qty,
quantity_fact=0,
status='work',
is_synced_1c=False,
)
return redirect('planning')
class ProductionTaskCreateView(LoginRequiredMixin, FormView):
template_name = 'shiftflow/task_create.html'
form_class = ProductionTaskCreateForm
success_url = reverse_lazy('planning')
def dispatch(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return redirect('registry')
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
profile = getattr(self.request.user, 'profile', None)
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
context['user_role'] = role
context['companies'] = Company.objects.all().order_by('name')
context['material_categories'] = MaterialCategory.objects.all().order_by('name')
context['steel_grades'] = SteelGrade.objects.all().order_by('name')
return context
def form_valid(self, form):
task = ProductionTask(
deal=form.cleaned_data['deal'],
drawing_name=form.cleaned_data.get('drawing_name') or 'Б/ч',
size_value=form.cleaned_data['size_value'],
material=form.cleaned_data['material'],
quantity_ordered=form.cleaned_data['quantity_ordered'],
is_bend=form.cleaned_data.get('is_bend') or False,
)
if form.cleaned_data.get('drawing_file'):
task.drawing_file = form.cleaned_data['drawing_file']
if form.cleaned_data.get('extra_drawing'):
task.extra_drawing = form.cleaned_data['extra_drawing']
task.save()
return super().form_valid(form)
class DealDetailView(LoginRequiredMixin, View):
def get(self, request, pk, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return JsonResponse({'error': 'forbidden'}, status=403)
deal = get_object_or_404(Deal, pk=pk)
return JsonResponse({
'id': deal.id,
'number': deal.number,
'company_id': deal.company_id,
'description': deal.description or '',
})
class DealUpsertView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return JsonResponse({'error': 'forbidden'}, status=403)
deal_id = request.POST.get('id')
number = (request.POST.get('number') or '').strip()
description = (request.POST.get('description') or '').strip()
company_id = request.POST.get('company_id')
if not number:
return JsonResponse({'error': 'number_required'}, status=400)
if deal_id and str(deal_id).isdigit():
deal = get_object_or_404(Deal, pk=int(deal_id))
deal.number = number
else:
deal, _ = Deal.objects.get_or_create(number=number)
deal.description = description
if company_id and str(company_id).isdigit():
deal.company_id = int(company_id)
else:
deal.company_id = None
deal.save()
return JsonResponse({'id': deal.id, 'label': deal.number})
class MaterialDetailView(LoginRequiredMixin, View):
def get(self, request, pk, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return JsonResponse({'error': 'forbidden'}, status=403)
material = get_object_or_404(Material, pk=pk)
return JsonResponse({
'id': material.id,
'category_id': material.category_id,
'steel_grade_id': material.steel_grade_id,
'name': material.name,
'full_name': material.full_name,
})
class MaterialUpsertView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return JsonResponse({'error': 'forbidden'}, status=403)
material_id = request.POST.get('id')
category_id = request.POST.get('category_id')
steel_grade_id = request.POST.get('steel_grade_id')
name = (request.POST.get('name') or '').strip()
if not (category_id and str(category_id).isdigit() and name):
return JsonResponse({'error': 'invalid'}, status=400)
if material_id and str(material_id).isdigit():
material = get_object_or_404(Material, pk=int(material_id))
else:
material = Material()
material.category_id = int(category_id)
material.name = name
if steel_grade_id and str(steel_grade_id).isdigit():
material.steel_grade_id = int(steel_grade_id)
else:
material.steel_grade_id = None
material.save()
return JsonResponse({'id': material.id, 'label': material.full_name})
class CompanyUpsertView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return JsonResponse({'error': 'forbidden'}, status=403)
company_id = request.POST.get('id')
name = (request.POST.get('name') or '').strip()
description = (request.POST.get('description') or '').strip()
if not name:
return JsonResponse({'error': 'name_required'}, status=400)
if company_id and str(company_id).isdigit():
company = get_object_or_404(Company, pk=int(company_id))
company.name = name
else:
company, _ = Company.objects.get_or_create(name=name)
company.description = description
company.save()
return JsonResponse({'id': company.id, 'label': company.name})
class MaterialCategoryUpsertView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return JsonResponse({'error': 'forbidden'}, status=403)
category_id = request.POST.get('id')
name = (request.POST.get('name') or '').strip()
gost_standard = (request.POST.get('gost_standard') or '').strip()
if not name:
return JsonResponse({'error': 'name_required'}, status=400)
if category_id and str(category_id).isdigit():
category = get_object_or_404(MaterialCategory, pk=int(category_id))
category.name = name
else:
category, _ = MaterialCategory.objects.get_or_create(name=name)
category.gost_standard = gost_standard
category.save()
return JsonResponse({'id': category.id, 'label': category.name})
class SteelGradeUpsertView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
if role not in ['admin', 'technologist']:
return JsonResponse({'error': 'forbidden'}, status=403)
grade_id = request.POST.get('id')
name = (request.POST.get('name') or '').strip()
gost_standard = (request.POST.get('gost_standard') or '').strip()
if not name:
return JsonResponse({'error': 'name_required'}, status=400)
if grade_id and str(grade_id).isdigit():
grade = get_object_or_404(SteelGrade, pk=int(grade_id))
grade.name = name
else:
grade, _ = SteelGrade.objects.get_or_create(name=name)
grade.gost_standard = gost_standard
grade.save()
return JsonResponse({'id': grade.id, 'label': grade.name})
# Вьюха детального вида и редактирования # Вьюха детального вида и редактирования
class ItemUpdateView(LoginRequiredMixin, UpdateView): class ItemUpdateView(LoginRequiredMixin, UpdateView):
model = Item model = Item

View File

@@ -16,7 +16,9 @@
</li> </li>
{% if user_role in 'admin,technologist' %} {% if user_role in 'admin,technologist' %}
<li class="nav-item"><a class="nav-link" href="#">Планирование</a></li> <li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'planning' %}active{% endif %}" href="{% url 'planning' %}">Планирование</a>
</li>
{% endif %} {% endif %}
{% if user_role in 'admin,technologist,master,operator' %} {% if user_role in 'admin,technologist,master,operator' %}