Доработали генерацию сменных заданий
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s

This commit is contained in:
2026-03-29 23:19:13 +03:00
parent 6013d5854b
commit ff0b791a24
10 changed files with 311 additions and 18 deletions

View File

@@ -53,8 +53,11 @@ class ItemAdmin(admin.ModelAdmin):
return obj.task.drawing_name if obj.task else "-" return obj.task.drawing_name if obj.task else "-"
get_drawing.short_description = 'Деталь' get_drawing.short_description = 'Деталь'
# Регистрация станков просто списком @admin.register(Machine)
admin.site.register(Machine) class MachineAdmin(admin.ModelAdmin):
list_display = ('name', 'machine_type')
list_filter = ('machine_type',)
search_fields = ('name',)
@admin.register(EmployeeProfile) @admin.register(EmployeeProfile)
class EmployeeProfileAdmin(admin.ModelAdmin): class EmployeeProfileAdmin(admin.ModelAdmin):

View 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='Тип станка'),
),
]

View File

@@ -20,9 +20,18 @@ class Machine(models.Model):
Список производственных участков (станков). Список производственных участков (станков).
Используется для фильтрации сменных заданий для конкретных операторов. Используется для фильтрации сменных заданий для конкретных операторов.
""" """
name = models.CharField("Название станка", max_length=100)
def __str__(self): return self.name 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
class Meta: class Meta:
verbose_name = "Станок"; verbose_name_plural = "Станки" verbose_name = "Станок"; verbose_name_plural = "Станки"

View File

@@ -43,9 +43,9 @@
</div> </div>
{% if user_role in 'admin,technologist,clerk' %} {% 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> <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="" {% if not is_synced %}selected{% endif %}>Все</option>
<option value="1" {% if is_synced == '1' %}selected{% endif %}>Учтено</option> <option value="1" {% if is_synced == '1' %}selected{% endif %}>Учтено</option>
<option value="0" {% if is_synced == '0' %}selected{% endif %}>Ожидает</option> <option value="0" {% if is_synced == '0' %}selected{% endif %}>Ожидает</option>
@@ -53,13 +53,13 @@
</div> </div>
{% endif %} {% endif %}
<div class="col-md-2"> <div class="col-md-auto">
<label class="small text-muted mb-1 fw-bold">С:</label> <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>
<div class="col-md-2"> <div class="col-md-auto">
<label class="small text-muted mb-1 fw-bold">По:</label> <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>
<div class="col-md-1 text-end mt-auto"> <div class="col-md-1 text-end mt-auto">

View File

@@ -4,8 +4,13 @@
{% include 'shiftflow/partials/_filter.html' %} {% include 'shiftflow/partials/_filter.html' %}
<div class="card shadow border-secondary"> <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> <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>
<div class="card-body p-0"> <div class="card-body p-0">

View 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>

View File

@@ -1,5 +1,10 @@
from django.urls import path from django.urls import path
from .views import IndexView, ItemUpdateView, RegistryView from .views import (
IndexView,
ItemUpdateView,
RegistryView,
RegistryPrintView,
)
urlpatterns = [ urlpatterns = [
# Главная страница (путь пустой) # Главная страница (путь пустой)
@@ -7,5 +12,7 @@ urlpatterns = [
# Реестр # Реестр
path('registry/', RegistryView.as_view(), name='registry'), 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'), path('item/<int:pk>/', ItemUpdateView.as_view(), name='item_detail'),
] ]

View File

@@ -1,3 +1,5 @@
from datetime import datetime
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic import TemplateView, ListView, UpdateView from django.views.generic import TemplateView, ListView, UpdateView
@@ -103,6 +105,97 @@ class RegistryView(LoginRequiredMixin, ListView):
return context 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): class ItemUpdateView(LoginRequiredMixin, UpdateView):
model = Item model = Item

View File

@@ -117,6 +117,14 @@ body {
color: #ffffff !important; color: #ffffff !important;
} }
.registry-filter-date {
width: 130px;
}
.registry-filter-1c {
width: 120px;
}
/* Специальный класс для центрирования окна логина (вернем его только там) */ /* Специальный класс для центрирования окна логина (вернем его только там) */
.flex-center-center { .flex-center-center {
display: flex; display: flex;

View File

@@ -30,17 +30,19 @@ class Material(models.Model):
category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория") category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория")
steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, verbose_name="Марка стали", null=True, blank=True) steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, verbose_name="Марка стали", null=True, blank=True)
name = models.CharField("Наименование (размер/характеристики)", max_length=255) name = models.CharField("Наименование (размер/характеристики)", max_length=255)
full_name = models.CharField("Полное наименование", max_length=500, blank=True, help_text="Генерируется автоматически, если пусто") full_name = models.CharField("Полное наименование", max_length=500, blank=True, help_text="Генерируется автоматически")
class Meta: class Meta:
verbose_name = "Материал (номенклатура)" verbose_name = "Материал (номенклатура)"
verbose_name_plural = "Материалы" verbose_name_plural = "Материалы"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.full_name: category_part = (self.category.name or '').strip() if self.category_id else ''
grade_str = f" {self.steel_grade.name}" if self.steel_grade else "" name_part = (self.name or '').strip()
self.full_name = f"{self.category.name} {self.name}{grade_str}" grade_part = (self.steel_grade.name or '').strip() if self.steel_grade_id else ''
self.full_name = ' '.join([p for p in [category_part, name_part, grade_part] if p])
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): def __str__(self):
return self.full_name or f"{self.category.name} {self.name}" return self.full_name or ' '.join([p for p in [(self.category.name if self.category_id else ''), self.name, (self.steel_grade.name if self.steel_grade_id else '')] if p]).strip()