начал работать с интерфейсом
All checks were successful
Deploy MES Core / deploy (push) Successful in 9s

This commit is contained in:
tertelius
2026-03-29 00:24:39 +03:00
parent f86f0bfcd4
commit a4ba577206
19 changed files with 561 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
import os
from django.contrib import admin
from .models import Company, Machine, Deal, Material, Item
from .models import Company, EmployeeProfile, Machine, Deal, Material, Item
# --- Настройка отображения Компаний ---
@admin.register(Company)
@@ -11,7 +11,7 @@ class CompanyAdmin(admin.ModelAdmin):
# --- Настройка отображения Сделок ---
@admin.register(Deal)
class DealAdmin(admin.ModelAdmin):
list_display = ('number', 'company', 'created_at')
list_display = ('number', 'company')
search_fields = ('number', 'company__name')
list_filter = ('company',)
@@ -68,4 +68,9 @@ class ItemAdmin(admin.ModelAdmin):
return initial
# Регистрация станков просто списком
admin.site.register(Machine)
admin.site.register(Machine)
@admin.register(EmployeeProfile)
class EmployeeProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'role')
filter_horizontal = ('machines',) # Красивый выбор станков двумя колонками

View File

@@ -0,0 +1,29 @@
# Generated by Django 6.0.3 on 2026-03-28 15:05
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shiftflow', '0002_company_material_alter_item_options_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='EmployeeProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('admin', 'Администратор'), ('technologist', 'Технолог'), ('master', 'Мастер'), ('operator', 'Оператор'), ('clerk', 'Учетчик')], default='operator', max_length=20, verbose_name='Должность')),
('machines', models.ManyToManyField(blank=True, to='shiftflow.machine', verbose_name='Закрепленные станки')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
],
options={
'verbose_name': 'Профиль сотрудника',
'verbose_name_plural': 'Профили сотрудников',
},
),
]

View File

@@ -94,4 +94,26 @@ class Item(models.Model):
def __str__(self):
return f"{self.drawing_name} ({self.quantity_plan} шт.)"
class EmployeeProfile(models.Model):
ROLE_CHOICES = [
('admin', 'Администратор'),
('technologist', 'Технолог'),
('master', 'Мастер'),
('operator', 'Оператор'),
('clerk', 'Учетчик'),
]
# Связь 1 к 1 со стандартным юзером Django
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name='Пользователь')
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='operator', verbose_name='Должность')
# Привязка станков (можно выбрать несколько для одного оператора)
machines = models.ManyToManyField('Machine', blank=True, verbose_name='Закрепленные станки')
def __str__(self):
return f"{self.user.username} - {self.get_role_display()}"
class Meta:
verbose_name = 'Профиль сотрудника'
verbose_name_plural = 'Профили сотрудников'

View File

@@ -0,0 +1,65 @@
{% extends 'base.html' %}
{% block content %}
{% include 'shiftflow/partials/_filter.html' %}
{% if not selected_machines %}
<div class="alert alert-warning text-center mt-4">
<i class="bi bi-info-circle me-2"></i> Станки не выбраны. Выберите станки в фильтре для отображения позиций.
</div>
{% else %}
{% if in_work is not None %}
<h6 class="text-primary mt-4 fw-bold"><i class="bi bi-play-circle me-2"></i>В РАБОТЕ</h6>
<div class="table-responsive rounded border shadow-sm mb-4">
<table class="table table-hover align-middle mb-0">
<thead class="table-custom-header small fw-bold text-uppercase">
<tr>
<th style="width: 80px;">Сделка</th>
<th>Деталь / Чертеж</th>
<th>Материал</th>
<th class="text-center">План</th>
<th class="text-center">Факт</th>
{% if user_role in 'admin,technologist,master,operator' %}
<th class="text-end">Действия</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for item in in_work %}
<tr>
<td class="small">{{ item.deal.number }}</td>
<td><strong>{{ item.drawing_name }}</strong> <small class="text-muted">({{ item.machine.name }})</small></td>
<td><span class="badge bg-secondary opacity-75">{{ item.material.name }}</span></td>
<td class="text-center fw-bold">{{ item.quantity_plan }}</td>
<td class="text-center" style="width: 100px;">
<input type="number" class="form-control form-control-sm text-center" value="{{ item.quantity_fact|default:0 }}" {% if user_role == 'clerk' %}readonly{% endif %}>
</td>
{% if user_role in 'admin,technologist,master,operator' %}
<td class="text-end">
<button class="btn btn-sm btn-outline-success"><i class="bi bi-check2-all"></i></button>
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="6" class="text-center py-4 text-muted small">Активных заданий нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if backlog is not None %}
<h6 class="text-danger fw-bold"><i class="bi bi-exclamation-triangle me-2"></i>НЕДОДЕЛЫ (ХВОСТЫ)</h6>
<div class="table-responsive rounded border shadow-sm mb-4">
</div>
{% endif %}
{% if done_items is not None %}
<h6 class="text-success fw-bold"><i class="bi bi-check-circle me-2"></i>ЗАВЕРШЕНО</h6>
<div class="table-responsive rounded border shadow-sm">
</div>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow bg-dark text-light border-secondary">
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
<h3 class="text-accent mb-0">Реестр деталей</h3>
{% if user_role == 'admin' or user_role == 'technologist' %}
<button class="btn btn-sm btn-outline-accent">+ Добавить заказ</button>
{% endif %}
</div>
<div class="card-body">
<table class="table table-dark table-hover">
<thead>
<tr class="table-custom-header">
<th>ID</th>
<th>Наименование</th>
<th>Кол-во</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td><span class="badge bg-secondary">{{ item.status }}</span></td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center text-muted">Деталей пока нет</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block content %}
<div class="text-center">
<h1 class="text-accent mb-4 display-3 fw-bold">
<i class="bi bi-gear-fill me-3"></i>ShiftFlow
</h1>
<a href="{% url 'login' %}" class="btn btn-lg btn-outline-accent px-5 py-3 fw-bold shadow">
ВОЙТИ В СИСТЕМУ
</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
<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">
<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()">
<label class="btn btn-outline-accent btn-sm" for="m_{{ m.id }}">{{ m.name }}</label>
</div>
{% endfor %}
</div>
</div>
<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>
<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>
</div>
</div>
<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()">
</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()">
</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>
</div>
</form>
</div>
</div>

10
shiftflow/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path
from .views import IndexView, RegistryView
urlpatterns = [
# Главная страница (путь пустой)
path('', IndexView.as_view(), name='index'),
# Реестр
path('registry/', RegistryView.as_view(), name='registry'),
]

72
shiftflow/views copy.py Normal file
View File

@@ -0,0 +1,72 @@
from django.shortcuts import render
from .models import Item, Machine
from django.utils import timezone
from datetime import datetime
def items_list_view(request):
# Если не авторизован, просто отдаем пустой контекст для страницы входа
if not request.user.is_authenticated:
return render(request, 'shiftflow/items_list.html', {})
# ОПРЕДЕЛЕНИЕ РОЛИ (Ищем в группах Джанго или ставим суперюзера)
user_role = 'guest'
if request.user.is_superuser:
user_role = 'admin'
elif request.user.groups.filter(name='Технолог').exists():
user_role = 'technologist'
elif request.user.groups.filter(name='Мастер').exists():
user_role = 'master'
elif request.user.groups.filter(name='Оператор').exists():
user_role = 'operator'
elif request.user.groups.filter(name='Учетчик').exists():
user_role = 'clerk'
# ФИЛЬТРЫ
all_machines = Machine.objects.all()
all_machine_ids = list(all_machines.values_list('id', flat=True))
# Проверяем, был ли нажат фильтр (есть ли параметр 'filtered' в URL)
is_filtered = 'filtered' in request.GET
if is_filtered:
selected_machines = [int(x) for x in request.GET.getlist('m_ids')]
selected_statuses = request.GET.getlist('statuses')
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
else:
# Значения по умолчанию (при первом заходе или сбросе)
selected_machines = all_machine_ids
selected_statuses = ['work', 'partial'] # В работе и недоделы
start_date = timezone.now().strftime('%Y-%m-%d')
end_date = timezone.now().strftime('%Y-%m-%d')
# ВЫБОРКА ДАННЫХ
items = Item.objects.select_related('deal', 'material', 'machine')
# Защита логики: если станки не выбраны — список пуст
if not selected_machines:
items = Item.objects.none()
else:
items = items.filter(
machine_id__in=selected_machines,
status__in=selected_statuses,
date__range=[start_date, end_date]
)
# Разбиваем по статусам для вывода в разные таблицы (если они выбраны в фильтре)
in_work = items.filter(status='work') if 'work' in selected_statuses else None
backlog = items.filter(status='partial') if 'partial' in selected_statuses else None
done_items = items.filter(status='done') if 'done' in selected_statuses else None
context = {
'user_role': user_role,
'machines': all_machines,
'selected_machines': selected_machines,
'selected_statuses': selected_statuses,
'start_date': start_date,
'end_date': end_date,
'in_work': in_work,
'backlog': backlog,
'done_items': done_items,
}
return render(request, 'shiftflow/items_list.html', context)

View File

@@ -1,3 +1,32 @@
from django.shortcuts import render
from django.shortcuts import redirect
from django.views.generic import TemplateView, ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Item # Проверь, как точно называется твоя модель деталей/заказов
# Create your views here.
# Класс главной страницы (роутер)
class IndexView(TemplateView):
template_name = 'shiftflow/landing.html'
def get(self, request, *args, **kwargs):
# Если юзер авторизован — сразу отправляем его в реестр
if request.user.is_authenticated:
return redirect('registry')
# Если нет — показываем кнопку "Войти"
return super().get(request, *args, **kwargs)
# Класс реестра деталей (защищен LoginRequiredMixin)
class RegistryView(LoginRequiredMixin, ListView):
model = Item
template_name = 'shiftflow/items_list.html'
context_object_name = 'items'
def get_queryset(self):
# Позже здесь добавим: .filter(machine__in=request.user.profile.machines.all())
return Item.objects.all().order_by('-id')
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
return context