Доработали фильт в реестре заданий
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s

This commit is contained in:
2026-03-29 20:29:05 +03:00
parent 7ef7409c7a
commit 6013d5854b
22 changed files with 431 additions and 63 deletions

View File

@@ -1,6 +1,6 @@
import os
from django.contrib import admin
from .models import Company, EmployeeProfile, Machine, Deal, Material, ProductionTask, Item
from .models import Company, EmployeeProfile, Machine, Deal, ProductionTask, Item
# --- Настройка отображения Компаний ---
@admin.register(Company)
@@ -15,11 +15,6 @@ class DealAdmin(admin.ModelAdmin):
search_fields = ('number', 'company__name')
list_filter = ('company',)
# --- Настройка отображения Материалов ---
@admin.register(Material)
class MaterialAdmin(admin.ModelAdmin):
search_fields = ('name',)
# --- Задания на производство (База) ---
@admin.register(ProductionTask)
class ProductionTaskAdmin(admin.ModelAdmin):
@@ -50,10 +45,12 @@ class ItemAdmin(admin.ModelAdmin):
}),
)
def get_deal(self, obj): return obj.task.deal
def get_deal(self, obj):
return obj.task.deal if obj.task else "-"
get_deal.short_description = 'Сделка'
def get_drawing(self, obj): return obj.task.drawing_name
def get_drawing(self, obj):
return obj.task.drawing_name if obj.task else "-"
get_drawing.short_description = 'Деталь'
# Регистрация станков просто списком

View File

@@ -0,0 +1,23 @@
# Generated by Django 6.0.3 on 2026-03-29 14:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shiftflow', '0004_alter_item_options_remove_item_deal_and_more'),
('warehouse', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='productiontask',
name='material',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='warehouse.material', verbose_name='Материал'),
),
migrations.DeleteModel(
name='Material',
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 6.0.3 on 2026-03-29 16:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shiftflow', '0005_alter_productiontask_material_delete_material'),
]
operations = [
migrations.AlterModelOptions(
name='item',
options={'ordering': ['-date', 'task__deal'], 'verbose_name': 'Пункт сменки', 'verbose_name_plural': 'Реестр сменных заданий'},
),
migrations.AlterField(
model_name='item',
name='status',
field=models.CharField(choices=[('work', 'В работе'), ('done', 'Выполнено'), ('partial', 'Частично'), ('leftover', 'Недодел'), ('imported', 'Импортировано')], default='work', max_length=10, verbose_name='Статус'),
),
]

View File

@@ -1,6 +1,7 @@
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from warehouse.models import Material as WarehouseMaterial
class Company(models.Model):
"""
@@ -38,17 +39,6 @@ class Deal(models.Model):
class Meta:
verbose_name = "Сделка"; verbose_name_plural = "Сделки"
class Material(models.Model):
"""
Справочник ТМЦ (Трубы, листы, профили).
Необходим для точного списания остатков и синхронизации с 1С.
"""
name = models.CharField("Наименование", max_length=255, unique=True)
def __str__(self): return self.name
class Meta:
verbose_name = "Материал"; verbose_name_plural = "Материалы"
class ProductionTask(models.Model):
"""
Основание для производства. Определяет ЧТО делать.
@@ -62,7 +52,7 @@ class ProductionTask(models.Model):
drawing_file = models.FileField("Исходник (DXF/IGES)", upload_to="drawings/%Y/%m/", blank=True, null=True)
extra_drawing = models.FileField("Доп. чертеж (PDF)", upload_to="extra_drawings/%Y/%m/", blank=True, null=True)
material = models.ForeignKey(Material, on_delete=models.PROTECT, verbose_name="Материал")
material = models.ForeignKey(WarehouseMaterial, on_delete=models.PROTECT, verbose_name="Материал")
quantity_ordered = models.PositiveIntegerField("Заказано всего, шт")
is_bend = models.BooleanField("Гибка", default=False)
@@ -84,6 +74,7 @@ class Item(models.Model):
('done', 'Выполнено'),
('partial', 'Частично'),
('leftover', 'Недодел'),
('imported', 'Импортировано'),
]
# --- Ссылка на основу (временно null=True для миграции старых данных) ---
@@ -106,11 +97,13 @@ class Item(models.Model):
is_synced_1c = models.BooleanField("Учтено в 1С", default=False)
class Meta:
verbose_name = "Позиция сменки"; verbose_name_plural = "Реестр сменных заданий"
verbose_name = "Пункт сменки"; verbose_name_plural = "Реестр сменных заданий"
ordering = ['-date', 'task__deal']
def __str__(self):
return f"{self.task.drawing_name} - {self.date}"
if self.task:
return f"{self.task.drawing_name} - {self.date}"
return f"Без задания - {self.date}"
class EmployeeProfile(models.Model):

View File

@@ -52,6 +52,21 @@
<label class="small text-muted">Сколько сделано?</label>
<input type="number" name="quantity_fact" id="id_quantity_fact" class="form-control form-control-lg text-center mx-auto" style="max-width: 200px;" value="{{ item.quantity_fact }}" max="{{ item.quantity_plan }}">
</div>
<div class="row g-3 mt-3 text-start">
<div class="col-md-4">
<label class="small text-muted">Взятый материал</label>
<input type="text" name="material_taken" class="form-control border-secondary" value="{{ item.material_taken }}" placeholder="Напр: 3 трубы по 12м">
</div>
<div class="col-md-4">
<label class="small text-muted">Деловой отход</label>
<input type="text" name="usable_waste" class="form-control border-secondary" value="{{ item.usable_waste }}" placeholder="Напр: кусок 1500мм">
</div>
<div class="col-md-4">
<label class="small text-muted">Лом (кг)</label>
<input type="number" step="0.01" name="scrap_weight" class="form-control border-secondary" value="{{ item.scrap_weight }}">
</div>
</div>
</div>
{% else %}
<div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div>
@@ -120,7 +135,7 @@ function closeTask(status) {
function showPartial() {
document.getElementById('partialInput').classList.remove('d-none');
document.getElementById('id_status').value = 'part'; // Статус Частично
document.getElementById('id_status').value = 'partial';
}
</script>
{% endblock %}

View File

@@ -1,45 +1,81 @@
<div class="card border-secondary mb-3 shadow-sm">
<div class="card-body py-2">
<form method="get" id="filter-form" class="row g-2 align-items-center">
<input type="hidden" name="filtered" value="1"> <div class="col-md-4">
<input type="hidden" name="filtered" value="1">
{% if user_role != 'operator' %}
<div class="col-md-4">
<div class="small text-muted mb-1 fw-bold">Станки:</div>
<div class="d-flex flex-wrap gap-1">
{% for m in machines %}
<div>
<input type="checkbox" class="btn-check" name="m_ids" id="m_{{ m.id }}" value="{{ m.id }}"
{% if m.id in selected_machines %}checked{% endif %} onchange="this.form.submit()">
{% if all_selected_machines or m.id in selected_machines %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-accent btn-sm" for="m_{{ m.id }}">{{ m.name }}</label>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="col-md-3">
<div class="small text-muted mb-1 fw-bold">Статус:</div>
<div class="d-flex flex-wrap gap-1">
<input type="checkbox" class="btn-check" name="statuses" id="s_work" value="work" {% if 'work' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_partial" value="partial" {% if 'partial' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-danger btn-sm" for="s_partial">Недодел</label>
{% if user_role == 'operator' %}
<input type="hidden" name="statuses" value="work">
<span class="badge bg-primary">В работе</span>
{% else %}
<input type="checkbox" class="btn-check" name="statuses" id="s_work" value="work" {% if 'work' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-primary btn-sm" for="s_work">В работе</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_leftover" value="leftover" {% if 'leftover' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-danger btn-sm" for="s_leftover">Недодел</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_done" value="done" {% if 'done' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-success btn-sm" for="s_done">Завершено</label>
<input type="checkbox" class="btn-check" name="statuses" id="s_closed" value="closed" {% if 'closed' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-success btn-sm" for="s_closed">Завершено</label>
{% if user_role in 'admin,technologist' %}
<input type="checkbox" class="btn-check" name="statuses" id="s_imported" value="imported" {% if 'imported' in selected_statuses %}checked{% endif %} onchange="this.form.submit()">
<label class="btn btn-outline-accent btn-sm" for="s_imported">Импорт</label>
{% endif %}
{% endif %}
</div>
</div>
{% if user_role in 'admin,technologist,clerk' %}
<div class="col-md-2">
<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()">
<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>
</select>
</div>
{% endif %}
<div class="col-md-2">
<label class="small text-muted mb-1 fw-bold">С:</label>
<input type="date" name="start_date" class="form-control form-control-sm" 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" value="{{ start_date }}" onchange="this.form.submit()">
</div>
<div class="col-md-2">
<label class="small text-muted mb-1 fw-bold">По:</label>
<input type="date" name="end_date" class="form-control form-control-sm" 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" value="{{ end_date }}" onchange="this.form.submit()">
</div>
<div class="col-md-1 text-end mt-auto">
<a href="{% url 'registry' %}" class="btn btn-outline-secondary btn-sm w-100" title="Сбросить по умолчанию"><i class="bi bi-x-circle"></i></a>
<a href="{% url 'registry' %}" class="btn btn-outline-secondary btn-sm w-100" title="Сброс">
<i class="bi bi-arrow-counterclockwise me-1"></i>Сброс
</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function(){
const s = document.querySelector('input[name="start_date"]');
const e = document.querySelector('input[name="end_date"]');
const today = new Date().toISOString().slice(0,10);
if (s && !s.value) s.value = today;
if (e && !e.value) e.value = today;
});
</script>
</div>
</div>

View File

@@ -1,6 +1,8 @@
{% extends 'base.html' %}
{% block content %}
{% include 'shiftflow/partials/_filter.html' %}
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3">
<h3 class="text-accent mb-0"><i class="bi bi-list-task me-2"></i>Реестр заданий</h3>
@@ -27,23 +29,23 @@
{% for item in items %}
<tr class="clickable-row" data-href="{% url 'item_detail' item.pk %}">
<td class="small">{{ item.date|date:"d.m.y" }}</td>
<td><span class="text-accent fw-bold">{{ item.deal.number }}</span></td>
<td><span class="text-accent fw-bold">{{ item.task.deal.number|default:"-" }}</span></td>
<td><span class="badge bg-dark border border-secondary">{{ item.machine.name }}</span></td>
<td class="fw-bold">{{ item.drawing_name }}</td>
<td class="small">{{ item.size_value }}</td>
<td class="fw-bold">{{ item.task.drawing_name|default:"Б/ч" }}</td>
<td class="small">{{ item.task.size_value|default:"-" }}</td>
<td>
<span class="text-info fw-bold">{{ item.quantity_plan }}</span> /
<span class="text-success">{{ item.quantity_fact }}</span>
</td>
<td class="small text-muted">{{ item.material.name }}</td>
<td class="small text-muted">{{ item.task.material.full_name|default:item.task.material.name|default:"-" }}</td>
<td class="text-center">
{% if item.drawing_file %}
<a href="{{ item.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/STEP">
{% if item.task.drawing_file %}
<a href="{{ item.task.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/STEP">
<i class="bi bi-file-earmark-code"></i>
</a>
{% endif %}
{% if item.extra_drawing %}
<a href="{{ item.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
{% if item.task.extra_drawing %}
<a href="{{ item.task.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 %}

View File

@@ -2,7 +2,8 @@ from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views.generic import TemplateView, ListView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Item # Проверь, как точно называется твоя модель деталей/заказов
from django.utils import timezone
from .models import Item, Machine
# Класс главной страницы (роутер)
class IndexView(TemplateView):
@@ -22,22 +23,84 @@ class RegistryView(LoginRequiredMixin, ListView):
context_object_name = 'items'
def get_queryset(self):
# Оптимизируем запросы, подгружая связанные данные сразу
queryset = Item.objects.select_related('task', 'task__deal', 'task__material', 'machine').all()
# Если это оператор, показываем только задания для его станков
if hasattr(self.request.user, 'profile') and self.request.user.profile.role == 'operator':
user_machines = self.request.user.profile.machines.all()
if user_machines.exists():
queryset = queryset.filter(machine__in=user_machines)
queryset = Item.objects.select_related('task', 'task__deal', 'task__material', 'machine')
user = self.request.user
profile = getattr(user, 'profile', None)
role = profile.role if profile else 'operator'
filtered = self.request.GET.get('filtered')
# Станки
m_ids = self.request.GET.getlist('m_ids')
if filtered and role != 'operator' and not m_ids:
return queryset.none()
if m_ids:
queryset = queryset.filter(machine_id__in=m_ids)
# Статусы (+ агрегат "closed" = done+partial)
statuses = self.request.GET.getlist('statuses')
if filtered and not statuses:
return 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'])
else:
if start_date:
queryset = queryset.filter(date__gte=start_date)
if end_date:
queryset = queryset.filter(date__lte=end_date)
# Списание (1С)
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 == 'operator':
user_machines = profile.machines.all() if profile else Machine.objects.none()
queryset = queryset.filter(machine__in=user_machines, status='work')
elif role == 'master' and not filtered:
queryset = queryset.filter(status='work')
return queryset.order_by('status', '-date', 'machine__name', 'task__deal__number')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Передаем роль в шаблон, чтобы скрывать/показывать кнопки
if hasattr(self.request.user, 'profile'):
context['user_role'] = self.request.user.profile.role
user = self.request.user
profile = getattr(user, 'profile', None)
role = profile.role if profile else 'operator'
context['user_role'] = role
machines = Machine.objects.all()
context['machines'] = machines
filtered = self.request.GET.get('filtered')
if not filtered:
today_str = timezone.now().date().strftime('%Y-%m-%d')
context['start_date'] = today_str
context['end_date'] = today_str
context['selected_statuses'] = ['work', 'leftover']
context['selected_machines'] = [m.id for m in machines]
context['all_selected_machines'] = True
else:
context['selected_machines'] = [int(i) for i in self.request.GET.getlist('m_ids') if i.isdigit()]
context['selected_statuses'] = self.request.GET.getlist('statuses')
context['start_date'] = self.request.GET.get('start_date', '')
context['end_date'] = self.request.GET.get('end_date', '')
context['is_synced'] = self.request.GET.get('is_synced', '')
context['all_selected_machines'] = False
return context
# Вьюха детального вида и редактирования
@@ -59,6 +122,54 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
context['user_role'] = self.request.user.profile.role
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
profile = getattr(request.user, 'profile', None)
role = profile.role if profile else 'operator'
# Общие поля
self.object.material_taken = request.POST.get('material_taken', self.object.material_taken)
self.object.usable_waste = request.POST.get('usable_waste', self.object.usable_waste)
self.object.scrap_weight = request.POST.get('scrap_weight', self.object.scrap_weight or 0)
status = request.POST.get('status', self.object.status)
if role in ['operator', 'master']:
if status == 'done':
self.object.quantity_fact = self.object.quantity_plan
self.object.status = 'done'
self.object.save()
elif status == 'partial':
try:
fact = int(request.POST.get('quantity_fact', '0'))
except ValueError:
fact = 0
fact = max(0, min(fact, self.object.quantity_plan))
residual = self.object.quantity_plan - fact
self.object.quantity_fact = fact
self.object.status = 'partial'
self.object.save()
if residual > 0:
Item.objects.create(
task=self.object.task,
date=self.object.date,
machine=self.object.machine,
quantity_plan=residual,
quantity_fact=0,
status='leftover',
is_synced_1c=False,
)
else:
# Просто сохранить без спец-логики
return super().post(request, *args, **kwargs)
elif role == 'clerk':
# Учетчик может отмечать списание 1С
self.object.is_synced_1c = bool(request.POST.get('is_synced_1c'))
self.object.save()
else:
return super().post(request, *args, **kwargs)
return redirect('registry')
def get_success_url(self):
# После сохранения возвращаемся в реестр
return reverse_lazy('registry')