Доработали генерацию сменных заданий
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
This commit is contained in:
@@ -53,8 +53,11 @@ class ItemAdmin(admin.ModelAdmin):
|
||||
return obj.task.drawing_name if obj.task else "-"
|
||||
get_drawing.short_description = 'Деталь'
|
||||
|
||||
# Регистрация станков просто списком
|
||||
admin.site.register(Machine)
|
||||
@admin.register(Machine)
|
||||
class MachineAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'machine_type')
|
||||
list_filter = ('machine_type',)
|
||||
search_fields = ('name',)
|
||||
|
||||
@admin.register(EmployeeProfile)
|
||||
class EmployeeProfileAdmin(admin.ModelAdmin):
|
||||
|
||||
18
shiftflow/migrations/0007_machine_machine_type.py
Normal file
18
shiftflow/migrations/0007_machine_machine_type.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-29 19:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shiftflow', '0006_alter_item_options_alter_item_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='machine',
|
||||
name='machine_type',
|
||||
field=models.CharField(choices=[('linear', 'Линейный'), ('sheet', 'Листовой')], default='linear', max_length=10, verbose_name='Тип станка'),
|
||||
),
|
||||
]
|
||||
@@ -20,9 +20,18 @@ class Machine(models.Model):
|
||||
Список производственных участков (станков).
|
||||
Используется для фильтрации сменных заданий для конкретных операторов.
|
||||
"""
|
||||
|
||||
MACHINE_TYPE_CHOICES = [
|
||||
('linear', 'Линейный'),
|
||||
('sheet', 'Листовой'),
|
||||
]
|
||||
|
||||
name = models.CharField("Название станка", max_length=100)
|
||||
machine_type = models.CharField("Тип станка", max_length=10, choices=MACHINE_TYPE_CHOICES, default='linear')
|
||||
|
||||
def __str__(self): return self.name
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Станок"; verbose_name_plural = "Станки"
|
||||
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
</div>
|
||||
|
||||
{% if user_role in 'admin,technologist,clerk' %}
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-auto">
|
||||
<label class="small text-muted mb-1 fw-bold">Учёт 1С:</label>
|
||||
<select name="is_synced" class="form-select form-select-sm bg-body text-body border-secondary" onchange="this.form.submit()">
|
||||
<select name="is_synced" class="form-select form-select-sm bg-body text-body border-secondary registry-filter-1c" onchange="this.form.submit()">
|
||||
<option value="" {% if not is_synced %}selected{% endif %}>Все</option>
|
||||
<option value="1" {% if is_synced == '1' %}selected{% endif %}>Учтено</option>
|
||||
<option value="0" {% if is_synced == '0' %}selected{% endif %}>Ожидает</option>
|
||||
@@ -53,13 +53,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-auto">
|
||||
<label class="small text-muted mb-1 fw-bold">С:</label>
|
||||
<input type="date" name="start_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ start_date }}" onchange="this.form.submit()">
|
||||
<input type="date" name="start_date" class="form-control form-control-sm bg-body text-body border-secondary registry-filter-date" value="{{ start_date }}" onchange="this.form.submit()">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-auto">
|
||||
<label class="small text-muted mb-1 fw-bold">По:</label>
|
||||
<input type="date" name="end_date" class="form-control form-control-sm bg-body text-body border-secondary" value="{{ end_date }}" onchange="this.form.submit()">
|
||||
<input type="date" name="end_date" class="form-control form-control-sm bg-body text-body border-secondary registry-filter-date" value="{{ end_date }}" onchange="this.form.submit()">
|
||||
</div>
|
||||
|
||||
<div class="col-md-1 text-end mt-auto">
|
||||
|
||||
@@ -4,8 +4,13 @@
|
||||
{% include 'shiftflow/partials/_filter.html' %}
|
||||
|
||||
<div class="card shadow border-secondary">
|
||||
<div class="card-header border-secondary py-3">
|
||||
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
|
||||
<h3 class="text-accent mb-0"><i class="bi bi-list-task me-2"></i>Реестр заданий</h3>
|
||||
{% if user_role in 'admin,technologist,master' %}
|
||||
<a class="btn btn-outline-secondary btn-sm" target="_blank" href="{% url 'registry_print' %}?{{ request.GET.urlencode }}">
|
||||
<i class="bi bi-printer me-1"></i>Печать
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
|
||||
148
shiftflow/templates/shiftflow/registry_print.html
Normal file
148
shiftflow/templates/shiftflow/registry_print.html
Normal file
@@ -0,0 +1,148 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Сменное задание</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||
<style>
|
||||
body { background: #fff; color: #000; }
|
||||
.print-table { width: 100%; border-collapse: collapse; }
|
||||
.print-table th, .print-table td { border: 1px solid #000; padding: 4px 6px; font-size: 12px; vertical-align: top; }
|
||||
.print-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 8px; }
|
||||
.print-title { font-size: 16px; font-weight: 700; margin: 0; }
|
||||
.print-meta { font-size: 12px; }
|
||||
.center { text-align: center; }
|
||||
.blank { height: 18px; }
|
||||
.sign-row { display: flex; justify-content: space-between; gap: 24px; margin-top: 18px; font-size: 12px; }
|
||||
.sign-line { flex: 1; border-bottom: 1px solid #000; height: 16px; }
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.page { page-break-after: always; }
|
||||
.page:last-child { page-break-after: auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid my-3 no-print d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="window.print()">
|
||||
Печать
|
||||
</button>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'registry' %}?{{ request.GET.urlencode }}">
|
||||
Назад
|
||||
</a>
|
||||
<div class="ms-auto small text-muted">
|
||||
{{ printed_at|date:"d.m.Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for machine, items in groups %}
|
||||
<div class="container-fluid page my-3">
|
||||
<div class="print-header">
|
||||
<div>
|
||||
<h1 class="print-title">Сменное задание</h1>
|
||||
<div class="print-meta">Станок: <strong>{{ machine.name }}</strong></div>
|
||||
</div>
|
||||
<div class="print-meta text-end">
|
||||
{% if print_date %}Дата: <strong>{{ print_date|date:"d.m.y" }}</strong>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if machine.machine_type == 'sheet' %}
|
||||
<table class="print-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Материал</th>
|
||||
<th colspan="5">Изделие</th>
|
||||
<th colspan="2">Остаток</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Вид</th>
|
||||
<th style="width: 170px;">Размеры</th>
|
||||
|
||||
<th style="width: 90px;">План, шт</th>
|
||||
<th style="width: 90px;">Факт, шт</th>
|
||||
<th style="width: 90px;">Толщина, мм</th>
|
||||
<th>Наименование</th>
|
||||
<th style="width: 90px;">Сделка</th>
|
||||
|
||||
<th style="width: 90px;">ДО</th>
|
||||
<th style="width: 90px;">Отход, кг</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.task.material.full_name|default:item.task.material.name|default:"-" }}</td>
|
||||
<td class="blank"></td>
|
||||
|
||||
<td class="center">{{ item.quantity_plan }}</td>
|
||||
<td class="blank"></td>
|
||||
<td class="center">{{ item.task.size_value|default:"-" }}</td>
|
||||
<td>{{ item.task.drawing_name|default:"Б/ч" }}</td>
|
||||
<td class="center">{{ item.task.deal.number|default:"-" }}</td>
|
||||
|
||||
<td class="blank"></td>
|
||||
<td class="blank"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<table class="print-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3">Материал</th>
|
||||
<th colspan="5">Изделие</th>
|
||||
<th colspan="2">Остаток</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Вид</th>
|
||||
<th style="width: 90px;">Длина, мм</th>
|
||||
<th style="width: 90px;">Кол-во, шт</th>
|
||||
|
||||
<th style="width: 90px;">Длина, мм</th>
|
||||
<th style="width: 90px;">План, шт</th>
|
||||
<th style="width: 90px;">Факт, шт</th>
|
||||
<th>Чертеж (если есть)</th>
|
||||
<th style="width: 90px;">Сделка</th>
|
||||
|
||||
<th style="width: 90px;">ДО, мм</th>
|
||||
<th style="width: 90px;">Отход, кг</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.task.material.full_name|default:item.task.material.name|default:"-" }}</td>
|
||||
<td class="blank"></td>
|
||||
<td class="blank"></td>
|
||||
|
||||
<td class="center">{{ item.task.size_value|default:"-" }}</td>
|
||||
<td class="center">{{ item.quantity_plan }}</td>
|
||||
<td class="blank"></td>
|
||||
<td>{{ item.task.drawing_name|default:"Б/ч" }}</td>
|
||||
<td class="center">{{ item.task.deal.number|default:"-" }}</td>
|
||||
|
||||
<td class="blank"></td>
|
||||
<td class="blank"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<div class="sign-row">
|
||||
<div style="display:flex; gap:8px; align-items:flex-end;">
|
||||
<div>Оператор</div><div class="sign-line"></div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; align-items:flex-end;">
|
||||
<div>Выдал</div><div class="sign-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,10 @@
|
||||
from django.urls import path
|
||||
from .views import IndexView, ItemUpdateView, RegistryView
|
||||
from .views import (
|
||||
IndexView,
|
||||
ItemUpdateView,
|
||||
RegistryView,
|
||||
RegistryPrintView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Главная страница (путь пустой)
|
||||
@@ -7,5 +12,7 @@ urlpatterns = [
|
||||
|
||||
# Реестр
|
||||
path('registry/', RegistryView.as_view(), name='registry'),
|
||||
# Печать сменного листа
|
||||
path('registry/print/', RegistryPrintView.as_view(), name='registry_print'),
|
||||
path('item/<int:pk>/', ItemUpdateView.as_view(), name='item_detail'),
|
||||
]
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import TemplateView, ListView, UpdateView
|
||||
@@ -102,7 +104,98 @@ class RegistryView(LoginRequiredMixin, ListView):
|
||||
context['all_selected_machines'] = False
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class RegistryPrintView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'shiftflow/registry_print.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
role = profile.role if profile else 'operator'
|
||||
if role not in ['admin', 'technologist', 'master']:
|
||||
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 'operator'
|
||||
context['user_role'] = role
|
||||
|
||||
queryset = Item.objects.select_related('task', 'task__deal', 'task__material', 'task__material__category', 'machine')
|
||||
filtered = self.request.GET.get('filtered')
|
||||
|
||||
m_ids = self.request.GET.getlist('m_ids')
|
||||
if filtered and not m_ids:
|
||||
queryset = queryset.none()
|
||||
if m_ids:
|
||||
queryset = queryset.filter(machine_id__in=m_ids)
|
||||
|
||||
statuses = self.request.GET.getlist('statuses')
|
||||
if filtered and not statuses:
|
||||
queryset = queryset.none()
|
||||
if statuses:
|
||||
expanded = []
|
||||
for s in statuses:
|
||||
if s == 'closed':
|
||||
expanded += ['done', 'partial']
|
||||
else:
|
||||
expanded.append(s)
|
||||
queryset = queryset.filter(status__in=expanded)
|
||||
|
||||
start_date = self.request.GET.get('start_date')
|
||||
end_date = self.request.GET.get('end_date')
|
||||
if not filtered:
|
||||
today = timezone.now().date()
|
||||
queryset = queryset.filter(date=today, status__in=['work', 'leftover'])
|
||||
start_date = today.strftime('%Y-%m-%d')
|
||||
end_date = start_date
|
||||
else:
|
||||
if start_date:
|
||||
queryset = queryset.filter(date__gte=start_date)
|
||||
if end_date:
|
||||
queryset = queryset.filter(date__lte=end_date)
|
||||
|
||||
is_synced = self.request.GET.get('is_synced')
|
||||
if is_synced in ['0', '1']:
|
||||
queryset = queryset.filter(is_synced_1c=bool(int(is_synced)))
|
||||
|
||||
if role == 'master' and not filtered:
|
||||
queryset = queryset.filter(status='work')
|
||||
|
||||
items = list(queryset.order_by('machine__name', 'date', 'task__deal__number', 'id'))
|
||||
groups = {}
|
||||
for item in items:
|
||||
groups.setdefault(item.machine, []).append(item)
|
||||
|
||||
context['groups'] = list(groups.items())
|
||||
context['printed_at'] = timezone.now()
|
||||
context['end_date'] = end_date or ''
|
||||
|
||||
print_date_raw = end_date or start_date
|
||||
print_date = None
|
||||
if isinstance(print_date_raw, str) and print_date_raw:
|
||||
try:
|
||||
print_date = datetime.strptime(print_date_raw, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
print_date = None
|
||||
context['print_date'] = print_date
|
||||
|
||||
if start_date and end_date and start_date == end_date:
|
||||
context['date_label'] = start_date
|
||||
elif start_date and end_date:
|
||||
context['date_label'] = f"{start_date} — {end_date}"
|
||||
elif start_date:
|
||||
context['date_label'] = f"c {start_date}"
|
||||
elif end_date:
|
||||
context['date_label'] = f"по {end_date}"
|
||||
else:
|
||||
context['date_label'] = ''
|
||||
|
||||
return context
|
||||
|
||||
|
||||
# Вьюха детального вида и редактирования
|
||||
class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Item
|
||||
|
||||
Reference in New Issue
Block a user