Статус выполнеия задания на деталь, мастер может менять станки
All checks were successful
Deploy MES Core / deploy (push) Successful in 9s

This commit is contained in:
2026-04-02 08:21:09 +03:00
parent d0289f6aec
commit 7cb00792ca
4 changed files with 79 additions and 11 deletions

View File

@@ -25,7 +25,15 @@
<div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body"> <div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body">
<div class="col-md-4"> <div class="col-md-4">
<small class="text-muted d-block">Станок</small> <small class="text-muted d-block">Станок</small>
<strong>{{ item.machine.name }}</strong> {% if user_role == 'master' %}
<select name="machine" class="form-select border-secondary">
{% for m in machines %}
<option value="{{ m.id }}" {% if item.machine.id == m.id %}selected{% endif %}>{{ m.name }}</option>
{% endfor %}
</select>
{% else %}
<strong>{{ item.machine.name }}</strong>
{% endif %}
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<small class="text-muted d-block">Материал</small> <small class="text-muted d-block">Материал</small>

View File

@@ -4,7 +4,7 @@
<div class="card shadow border-secondary"> <div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center"> <div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-3">
<h3 class="text-accent mb-0"><i class="bi bi-kanban me-2"></i>Сделки</h3> <h3 class="text-accent mb-0"><i class="bi bi-briefcase me-2"></i>Сделки</h3>
<form method="get" class="d-flex align-items-center gap-2"> <form method="get" class="d-flex align-items-center gap-2">
<span class="small text-muted">Сделки:</span> <span class="small text-muted">Сделки:</span>
<div class="d-flex flex-wrap gap-1"> <div class="d-flex flex-wrap gap-1">

View File

@@ -34,10 +34,10 @@
<th>Деталь</th> <th>Деталь</th>
<th>Материал</th> <th>Материал</th>
<th>Размер</th> <th>Размер</th>
<th class="text-center">Надо</th> <th style="width: 160px;">Прогресс</th>
<th class="text-center">Сделано</th> <th class="text-center">Надо / Сделано / В плане</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> <th class="text-end">Действия</th>
</tr> </tr>
</thead> </thead>
@@ -47,10 +47,30 @@
<td class="fw-bold">{{ t.drawing_name|default:"Б/ч" }}</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 text-muted">{{ t.material.full_name|default:t.material.name }}</td>
<td class="small">{{ t.size_value }}</td> <td class="small">{{ t.size_value }}</td>
<td class="text-center">{{ t.quantity_ordered }}</td> <td>
<td class="text-center">{{ t.done_qty }}</td> <div class="progress bg-secondary-subtle border border-secondary sf-progress" style="height: 10px;" data-done-width="{{ t.done_width }}" data-plan-width="{{ t.plan_width }}" title="Сделано: {{ t.done_pct }}% · В плане: {{ t.plan_pct }}%">
<td class="text-center">{{ t.planned_qty }}</td> <div class="progress-bar bg-success sf-progress-done"></div>
<div class="progress-bar bg-warning sf-progress-plan"></div>
</div>
</td>
<td class="text-center">
<span class="text-info fw-bold">{{ t.quantity_ordered }}</span> /
<span class="text-success">{{ t.done_qty }}</span> /
<span class="text-warning">{{ t.planned_qty }}</span>
</td>
<td class="text-center">{{ t.remaining_qty }}</td> <td class="text-center">{{ t.remaining_qty }}</td>
<td class="text-center">
{% if t.drawing_file %}
<a href="{{ t.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/IGES">
<i class="bi bi-file-earmark-code"></i>
</a>
{% endif %}
{% if t.extra_drawing %}
<a href="{{ t.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
<i class="bi bi-file-pdf"></i>
</a>
{% endif %}
</td>
<td class="text-end"> <td class="text-end">
<button <button
type="button" type="button"
@@ -116,12 +136,21 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('tr.task-row[data-href]').forEach(function (row) { document.querySelectorAll('tr.task-row[data-href]').forEach(function (row) {
row.addEventListener('click', function (e) { row.addEventListener('click', function (e) {
if (e.target && e.target.closest('button')) return; if (e.target && (e.target.closest('button') || e.target.closest('.stop-prop'))) return;
const href = row.getAttribute('data-href'); const href = row.getAttribute('data-href');
if (href) window.location.href = href; if (href) window.location.href = href;
}); });
}); });
document.querySelectorAll('.sf-progress').forEach(function (el) {
const done = parseInt(el.getAttribute('data-done-width') || '0', 10) || 0;
const plan = parseInt(el.getAttribute('data-plan-width') || '0', 10) || 0;
const doneEl = el.querySelector('.sf-progress-done');
const planEl = el.querySelector('.sf-progress-plan');
if (doneEl) doneEl.style.width = `${done}%`;
if (planEl) planEl.style.width = `${plan}%`;
});
const modal = document.getElementById('addToPlanModal'); const modal = document.getElementById('addToPlanModal');
if (!modal) return; if (!modal) return;

View File

@@ -262,7 +262,7 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
deal = get_object_or_404(Deal.objects.select_related('company'), pk=self.kwargs['pk']) deal = get_object_or_404(Deal.objects.select_related('company'), pk=self.kwargs['pk'])
context['deal'] = deal context['deal'] = deal
tasks = ProductionTask.objects.filter(deal=deal).select_related('material').annotate( tasks_qs = ProductionTask.objects.filter(deal=deal).select_related('material').annotate(
done_qty=Coalesce(Sum('items__quantity_fact'), 0), done_qty=Coalesce(Sum('items__quantity_fact'), 0),
planned_qty=Coalesce( planned_qty=Coalesce(
Sum( Sum(
@@ -281,6 +281,27 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
) )
).order_by('-id') ).order_by('-id')
tasks = list(tasks_qs)
for t in tasks:
need = int(t.quantity_ordered or 0)
done_qty = int(t.done_qty or 0)
planned_qty = int(t.planned_qty or 0)
if need > 0:
done_pct = int(round(done_qty * 100 / need))
plan_pct = int(round(planned_qty * 100 / need))
else:
done_pct = 0
plan_pct = 0
done_width = max(0, min(100, done_pct))
plan_width = max(0, min(100 - done_width, plan_pct))
t.done_pct = done_pct
t.plan_pct = plan_pct
t.done_width = done_width
t.plan_width = plan_width
context['tasks'] = tasks context['tasks'] = tasks
context['machines'] = Machine.objects.all() context['machines'] = Machine.objects.all()
return context return context
@@ -695,6 +716,13 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
scrap_weight_raw = (request.POST.get('scrap_weight') or '').strip() scrap_weight_raw = (request.POST.get('scrap_weight') or '').strip()
status = request.POST.get('status', self.object.status) status = request.POST.get('status', self.object.status)
machine_changed = False
if role == 'master':
machine_id = request.POST.get('machine')
if machine_id and machine_id.isdigit():
self.object.machine_id = int(machine_id)
machine_changed = True
# Разрешаем мастеру редактировать операторские поля всегда, # Разрешаем мастеру редактировать операторские поля всегда,
# оператору — только в процессе закрытия # оператору — только в процессе закрытия
if role == 'operator' and self.object.status != 'work': if role == 'operator' and self.object.status != 'work':
@@ -765,7 +793,10 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
return redirect('registry') return redirect('registry')
# Если статус не менялся (или не 'work'), просто сохраняем поля # Если статус не менялся (или не 'work'), просто сохраняем поля
self.object.save(update_fields=['material_taken', 'usable_waste', 'scrap_weight']) update_fields = ['material_taken', 'usable_waste', 'scrap_weight']
if machine_changed:
update_fields.append('machine')
self.object.save(update_fields=update_fields)
return redirect('registry') return redirect('registry')
if role == 'clerk': if role == 'clerk':