This commit is contained in:
29
shiftflow/forms.py
Normal file
29
shiftflow/forms.py
Normal 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="— выбрать —",
|
||||||
|
)
|
||||||
122
shiftflow/templates/shiftflow/planning.html
Normal file
122
shiftflow/templates/shiftflow/planning.html
Normal 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 %}
|
||||||
584
shiftflow/templates/shiftflow/task_create.html
Normal file
584
shiftflow/templates/shiftflow/task_create.html
Normal 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 %}
|
||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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' %}
|
||||||
|
|||||||
Reference in New Issue
Block a user