Доработали фильт в реестре заданий
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

2
.env
View File

@@ -4,8 +4,6 @@ DB_USER=prodman_user
DB_PASS=prodman_password_zwE45t! DB_PASS=prodman_password_zwE45t!
# Настройки Django # Настройки Django
# ENV_TYPE=dev
# ENV_TYPE=server
DB_HOST=db DB_HOST=db
SECRET_KEY='django-insecure-9p*t_(vtwo144=u)ie8qzb31a8%i(&1w4$0vq#udautexj^vms' SECRET_KEY='django-insecure-9p*t_(vtwo144=u)ie8qzb31a8%i(&1w4$0vq#udautexj^vms'
# todo потом установить домен для продакшена # todo потом установить домен для продакшена

View File

@@ -63,6 +63,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'shiftflow', # Вот это допиши обязательно! 'shiftflow', # Вот это допиши обязательно!
'warehouse',
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@@ -16,10 +16,13 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.views.generic import RedirectView
from django.templatetags.static import static as static_url
from django.conf.urls.static import static # <--- Добавьте эту строку from django.conf.urls.static import static # <--- Добавьте эту строку
from core import settings from core import settings
urlpatterns = [ urlpatterns = [
path('favicon.ico', RedirectView.as_view(url=static_url('favicon.svg'), permanent=True)),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
# Добавь эту строку, она подключит login, logout и прочие стандартные пути # Добавь эту строку, она подключит login, logout и прочие стандартные пути
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),

View File

@@ -1,6 +1,6 @@
import os import os
from django.contrib import admin 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) @admin.register(Company)
@@ -15,11 +15,6 @@ class DealAdmin(admin.ModelAdmin):
search_fields = ('number', 'company__name') search_fields = ('number', 'company__name')
list_filter = ('company',) list_filter = ('company',)
# --- Настройка отображения Материалов ---
@admin.register(Material)
class MaterialAdmin(admin.ModelAdmin):
search_fields = ('name',)
# --- Задания на производство (База) --- # --- Задания на производство (База) ---
@admin.register(ProductionTask) @admin.register(ProductionTask)
class ProductionTaskAdmin(admin.ModelAdmin): 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 = 'Сделка' 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 = 'Деталь' 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.db import models
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.models import User from django.contrib.auth.models import User
from warehouse.models import Material as WarehouseMaterial
class Company(models.Model): class Company(models.Model):
""" """
@@ -38,17 +39,6 @@ class Deal(models.Model):
class Meta: class Meta:
verbose_name = "Сделка"; verbose_name_plural = "Сделки" 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): 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) 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) 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("Заказано всего, шт") quantity_ordered = models.PositiveIntegerField("Заказано всего, шт")
is_bend = models.BooleanField("Гибка", default=False) is_bend = models.BooleanField("Гибка", default=False)
@@ -84,6 +74,7 @@ class Item(models.Model):
('done', 'Выполнено'), ('done', 'Выполнено'),
('partial', 'Частично'), ('partial', 'Частично'),
('leftover', 'Недодел'), ('leftover', 'Недодел'),
('imported', 'Импортировано'),
] ]
# --- Ссылка на основу (временно null=True для миграции старых данных) --- # --- Ссылка на основу (временно null=True для миграции старых данных) ---
@@ -106,11 +97,13 @@ class Item(models.Model):
is_synced_1c = models.BooleanField("Учтено в 1С", default=False) is_synced_1c = models.BooleanField("Учтено в 1С", default=False)
class Meta: class Meta:
verbose_name = "Позиция сменки"; verbose_name_plural = "Реестр сменных заданий" verbose_name = "Пункт сменки"; verbose_name_plural = "Реестр сменных заданий"
ordering = ['-date', 'task__deal'] ordering = ['-date', 'task__deal']
def __str__(self): def __str__(self):
if self.task:
return f"{self.task.drawing_name} - {self.date}" return f"{self.task.drawing_name} - {self.date}"
return f"Без задания - {self.date}"
class EmployeeProfile(models.Model): class EmployeeProfile(models.Model):

View File

@@ -52,6 +52,21 @@
<label class="small text-muted">Сколько сделано?</label> <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 }}"> <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>
<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> </div>
{% else %} {% else %}
<div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div> <div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div>
@@ -120,7 +135,7 @@ function closeTask(status) {
function showPartial() { function showPartial() {
document.getElementById('partialInput').classList.remove('d-none'); document.getElementById('partialInput').classList.remove('d-none');
document.getElementById('id_status').value = 'part'; // Статус Частично document.getElementById('id_status').value = 'partial';
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,45 +1,81 @@
<div class="card border-secondary mb-3 shadow-sm"> <div class="card border-secondary mb-3 shadow-sm">
<div class="card-body py-2"> <div class="card-body py-2">
<form method="get" id="filter-form" class="row g-2 align-items-center"> <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="small text-muted mb-1 fw-bold">Станки:</div>
<div class="d-flex flex-wrap gap-1"> <div class="d-flex flex-wrap gap-1">
{% for m in machines %} {% for m in machines %}
<div> <div>
<input type="checkbox" class="btn-check" name="m_ids" id="m_{{ m.id }}" value="{{ m.id }}" <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> <label class="btn btn-outline-accent btn-sm" for="m_{{ m.id }}">{{ m.name }}</label>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endif %}
<div class="col-md-3"> <div class="col-md-3">
<div class="small text-muted mb-1 fw-bold">Статус:</div> <div class="small text-muted mb-1 fw-bold">Статус:</div>
<div class="d-flex flex-wrap gap-1"> <div class="d-flex flex-wrap gap-1">
{% 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()"> <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> <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()"> <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_partial">Недодел</label> <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()"> <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_done">Завершено</label> <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>
</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"> <div class="col-md-2">
<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" 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>
<div class="col-md-2"> <div class="col-md-2">
<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" 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>
<div class="col-md-1 text-end mt-auto"> <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> </div>
</form> </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>
</div> </div>

View File

@@ -1,6 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% 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">
<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>
@@ -27,23 +29,23 @@
{% for item in items %} {% for item in items %}
<tr class="clickable-row" data-href="{% url 'item_detail' item.pk %}"> <tr class="clickable-row" data-href="{% url 'item_detail' item.pk %}">
<td class="small">{{ item.date|date:"d.m.y" }}</td> <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><span class="badge bg-dark border border-secondary">{{ item.machine.name }}</span></td>
<td class="fw-bold">{{ item.drawing_name }}</td> <td class="fw-bold">{{ item.task.drawing_name|default:"Б/ч" }}</td>
<td class="small">{{ item.size_value }}</td> <td class="small">{{ item.task.size_value|default:"-" }}</td>
<td> <td>
<span class="text-info fw-bold">{{ item.quantity_plan }}</span> / <span class="text-info fw-bold">{{ item.quantity_plan }}</span> /
<span class="text-success">{{ item.quantity_fact }}</span> <span class="text-success">{{ item.quantity_fact }}</span>
</td> </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"> <td class="text-center">
{% if item.drawing_file %} {% if item.task.drawing_file %}
<a href="{{ item.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/STEP"> <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> <i class="bi bi-file-earmark-code"></i>
</a> </a>
{% endif %} {% endif %}
{% if item.extra_drawing %} {% if item.task.extra_drawing %}
<a href="{{ item.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF"> <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> <i class="bi bi-file-pdf"></i>
</a> </a>
{% endif %} {% endif %}

View File

@@ -2,7 +2,8 @@ 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
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Item # Проверь, как точно называется твоя модель деталей/заказов from django.utils import timezone
from .models import Item, Machine
# Класс главной страницы (роутер) # Класс главной страницы (роутер)
class IndexView(TemplateView): class IndexView(TemplateView):
@@ -22,22 +23,84 @@ class RegistryView(LoginRequiredMixin, ListView):
context_object_name = 'items' context_object_name = 'items'
def get_queryset(self): def get_queryset(self):
# Оптимизируем запросы, подгружая связанные данные сразу queryset = Item.objects.select_related('task', 'task__deal', 'task__material', 'machine')
queryset = Item.objects.select_related('task', 'task__deal', 'task__material', 'machine').all() user = self.request.user
profile = getattr(user, 'profile', None)
role = profile.role if profile else 'operator'
filtered = self.request.GET.get('filtered')
# Если это оператор, показываем только задания для его станков # Станки
if hasattr(self.request.user, 'profile') and self.request.user.profile.role == 'operator': m_ids = self.request.GET.getlist('m_ids')
user_machines = self.request.user.profile.machines.all() if filtered and role != 'operator' and not m_ids:
if user_machines.exists(): return queryset.none()
queryset = queryset.filter(machine__in=user_machines) 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') return queryset.order_by('status', '-date', 'machine__name', 'task__deal__number')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Передаем роль в шаблон, чтобы скрывать/показывать кнопки user = self.request.user
if hasattr(self.request.user, 'profile'): profile = getattr(user, 'profile', None)
context['user_role'] = self.request.user.profile.role 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 return context
# Вьюха детального вида и редактирования # Вьюха детального вида и редактирования
@@ -59,6 +122,54 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
context['user_role'] = self.request.user.profile.role context['user_role'] = self.request.user.profile.role
return context 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): def get_success_url(self):
# После сохранения возвращаемся в реестр
return reverse_lazy('registry') return reverse_lazy('registry')

View File

@@ -20,7 +20,8 @@ body {
.navbar .nav-link, .navbar .nav-link,
.navbar .navbar-brand, .navbar .navbar-brand,
.footer-custom span, .footer-custom span,
.footer-custom strong { .footer-custom strong,
.footer-custom .text-muted {
color: #e9ecef !important; color: #e9ecef !important;
} }
@@ -62,6 +63,11 @@ body {
--bs-accent: #0d6efd; /* Синий акцент для светлой темы */ --bs-accent: #0d6efd; /* Синий акцент для светлой темы */
} }
[data-bs-theme="dark"] input[type="date"] { color-scheme: dark; }
[data-bs-theme="dark"] .form-control[type="date"] { background-color: #1e1e1e; border-color: #3d4246; color: #e9ecef; }
[data-bs-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1) brightness(1.4) contrast(1.2); opacity: 0.95; }
[data-bs-theme="light"] input[type="date"] { color-scheme: light; }
/* --- ТАБЛИЦА И КАРТОЧКИ --- */ /* --- ТАБЛИЦА И КАРТОЧКИ --- */
/* Заголовок таблицы: всегда темный с акцентным текстом */ /* Заголовок таблицы: всегда темный с акцентным текстом */
@@ -90,10 +96,25 @@ body {
border-color: var(--bs-accent) !important; border-color: var(--bs-accent) !important;
} }
/* Состояние кнопки при наведении */ .btn-check:checked + .btn-outline-accent,
.btn-outline-accent:hover { .btn-outline-accent.active,
.btn-outline-accent:active {
background-color: var(--bs-accent) !important; background-color: var(--bs-accent) !important;
color: #000 !important; /* Текст становится черным для контраста */ border-color: var(--bs-accent) !important;
}
[data-bs-theme="dark"] .btn-outline-accent:hover,
[data-bs-theme="dark"] .btn-check:checked + .btn-outline-accent,
[data-bs-theme="dark"] .btn-outline-accent.active,
[data-bs-theme="dark"] .btn-outline-accent:active {
color: #212529 !important;
}
[data-bs-theme="light"] .btn-outline-accent:hover,
[data-bs-theme="light"] .btn-check:checked + .btn-outline-accent,
[data-bs-theme="light"] .btn-outline-accent.active,
[data-bs-theme="light"] .btn-outline-accent:active {
color: #ffffff !important;
} }
/* Специальный класс для центрирования окна логина (вернем его только там) */ /* Специальный класс для центрирования окна логина (вернем его только там) */

3
static/favicon.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="#ffc107" d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@@ -5,6 +5,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}ShiftFlow MES{% endblock %}</title> <title>{% block title %}ShiftFlow MES{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.svg' %}" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'css/style.css' %}"> <link rel="stylesheet" href="{% static 'css/style.css' %}">

0
warehouse/__init__.py Normal file
View File

19
warehouse/admin.py Normal file
View File

@@ -0,0 +1,19 @@
from django.contrib import admin
from .models import MaterialCategory, SteelGrade, Material
@admin.register(MaterialCategory)
class MaterialCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'gost_standard')
search_fields = ('name', 'gost_standard')
@admin.register(SteelGrade)
class SteelGradeAdmin(admin.ModelAdmin):
list_display = ('name', 'gost_standard')
search_fields = ('name', 'gost_standard')
@admin.register(Material)
class MaterialAdmin(admin.ModelAdmin):
list_display = ('full_name', 'category', 'steel_grade', 'name')
list_filter = ('category', 'steel_grade')
search_fields = ('name', 'full_name')
readonly_fields = ('full_name',)

6
warehouse/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class WarehouseConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'warehouse'
verbose_name = 'Склад и материалы'

View File

@@ -0,0 +1,53 @@
# 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):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='MaterialCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Название категории')),
],
options={
'verbose_name': 'Категория материала',
'verbose_name_plural': 'Категории материалов',
},
),
migrations.CreateModel(
name='SteelGrade',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Марка стали')),
('gost_standard', models.CharField(blank=True, help_text='Основной стандарт для этой марки', max_length=255, verbose_name='ГОСТ/ТУ')),
('certificate_pdf', models.FileField(blank=True, null=True, upload_to='certificates/', verbose_name='Сертификат/ГОСТ (PDF)')),
],
options={
'verbose_name': 'Марка стали',
'verbose_name_plural': 'Марки стали',
},
),
migrations.CreateModel(
name='Material',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Наименование (размер/характеристики)')),
('full_name', models.CharField(blank=True, help_text='Генерируется автоматически, если пусто', max_length=500, verbose_name='Полное наименование')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='warehouse.materialcategory', verbose_name='Категория')),
('steel_grade', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='warehouse.steelgrade', verbose_name='Марка стали')),
],
options={
'verbose_name': 'Материал (номенклатура)',
'verbose_name_plural': 'Материалы',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-03-29 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='materialcategory',
name='gost_standard',
field=models.CharField(blank=True, help_text='Напр: ГОСТ 8639-82', max_length=255, verbose_name='ГОСТ на тип проката'),
),
]

View File

46
warehouse/models.py Normal file
View File

@@ -0,0 +1,46 @@
from django.db import models
class MaterialCategory(models.Model):
"""Категория материала (например, Труба, Лист, Круг)"""
name = models.CharField("Название категории", max_length=100, unique=True)
gost_standard = models.CharField("ГОСТ на тип проката", max_length=255, blank=True, help_text="Напр: ГОСТ 8639-82")
class Meta:
verbose_name = "Категория материала"
verbose_name_plural = "Категории материалов"
def __str__(self):
return self.name
class SteelGrade(models.Model):
"""Марка стали (например, Ст3сп, 09Г2С) и связанные с ней ГОСТы"""
name = models.CharField("Марка стали", max_length=100, unique=True)
gost_standard = models.CharField("ГОСТ/ТУ", max_length=255, blank=True, help_text="Основной стандарт для этой марки")
certificate_pdf = models.FileField("Сертификат/ГОСТ (PDF)", upload_to='certificates/', blank=True, null=True)
class Meta:
verbose_name = "Марка стали"
verbose_name_plural = "Марки стали"
def __str__(self):
return f"{self.name} ({self.gost_standard})" if self.gost_standard else self.name
class Material(models.Model):
"""Конкретная номенклатурная единица (например, Труба 100х100х4)"""
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)
name = models.CharField("Наименование (размер/характеристики)", max_length=255)
full_name = models.CharField("Полное наименование", max_length=500, blank=True, help_text="Генерируется автоматически, если пусто")
class Meta:
verbose_name = "Материал (номенклатура)"
verbose_name_plural = "Материалы"
def save(self, *args, **kwargs):
if not self.full_name:
grade_str = f" {self.steel_grade.name}" if self.steel_grade else ""
self.full_name = f"{self.category.name} {self.name}{grade_str}"
super().save(*args, **kwargs)
def __str__(self):
return self.full_name or f"{self.category.name} {self.name}"

0
warehouse/views.py Normal file
View File