Поменял структуру моделей
Some checks failed
Deploy MES Core / deploy (push) Failing after 1s

This commit is contained in:
2026-03-29 16:04:02 +03:00
parent 641abfff5e
commit 191d06d7d3
11 changed files with 194 additions and 77 deletions

2
.env
View File

@@ -4,7 +4,7 @@ DB_USER=prodman_user
DB_PASS=prodman_password_zwE45t! DB_PASS=prodman_password_zwE45t!
# Настройки Django # Настройки Django
ENV_TYPE=dev # ENV_TYPE=dev
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

@@ -7,11 +7,12 @@
Если внес изменения в проект и готов отправить их на сервер `ProdServ`: Если внес изменения в проект и готов отправить их на сервер `ProdServ`:
1. **Подготовь файлы**: 1. **Подготовь файлы**:
`git add .` git add .
2. **Запечатай изменения**: 2. **Запечатай изменения**:
`git commit -m "Подправил докерфайл сборки контейнера"` git commit -m "Подправил докерфайл сборки контейнера"
3. **Отправляй в полет**: 3. **Отправляй в полет**:
`git push origin main` git push origin main
### 💊 Таблетка: Если не пушится (сброс авторизации) ### 💊 Таблетка: Если не пушится (сброс авторизации)
Если Git "забыл" пароль или выдает ошибку Permission Denied: Если Git "забыл" пароль или выдает ошибку Permission Denied:

View File

@@ -89,6 +89,7 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'shiftflow.context_processors.env_info', # Правильный путь
], ],
}, },
}, },

View File

@@ -20,6 +20,7 @@ services:
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env # Прокидывает все секреты и настройки внутрь Python - .env # Прокидывает все секреты и настройки внутрь Python
- ENV_TYPE=server
volumes: volumes:
# Общие папки для статики и картинок. Сюда Django их складывает. # Общие папки для статики и картинок. Сюда Django их складывает.
- staticfiles:/app/staticfiles - staticfiles:/app/staticfiles

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, Item from .models import Company, EmployeeProfile, Machine, Deal, Material, ProductionTask, Item
# --- Настройка отображения Компаний --- # --- Настройка отображения Компаний ---
@admin.register(Company) @admin.register(Company)
@@ -20,52 +20,41 @@ class DealAdmin(admin.ModelAdmin):
class MaterialAdmin(admin.ModelAdmin): class MaterialAdmin(admin.ModelAdmin):
search_fields = ('name',) search_fields = ('name',)
# --- ГЛАВНАЯ МАГИЯ: Сменные задания --- # --- Задания на производство (База) ---
@admin.register(ProductionTask)
class ProductionTaskAdmin(admin.ModelAdmin):
list_display = ('drawing_name', 'deal', 'material', 'quantity_ordered', 'created_at')
search_fields = ('drawing_name', 'deal__number')
list_filter = ('deal', 'material', 'is_bend')
# --- Сменные задания (Выполнение) ---
@admin.register(Item) @admin.register(Item)
class ItemAdmin(admin.ModelAdmin): class ItemAdmin(admin.ModelAdmin):
# Что видим в общем списке # Что видим в общем списке (используем task__ для доступа к полям базы)
list_display = ('date', 'machine', 'deal', 'drawing_name', 'quantity_plan', 'quantity_fact', 'status', 'is_synced_1c') list_display = ('date', 'machine', 'get_deal', 'get_drawing', 'quantity_plan', 'quantity_fact', 'status', 'is_synced_1c')
# Фильтры справа # Фильтры справа
list_filter = ('date', 'machine', 'status', 'is_synced_1c', 'deal') list_filter = ('date', 'machine', 'status', 'is_synced_1c', 'task__deal')
# Поиск по номеру сделки и названию детали # Поиск по номеру сделки и названию детали через связь task
search_fields = ('drawing_name', 'deal__number') search_fields = ('task__drawing_name', 'task__deal__number')
# Группируем поля в форме редактирования для удобства # Группируем поля в форме редактирования
fieldsets = ( fieldsets = (
('Основная информация', { ('Связь с заданием', {
'fields': ('date', 'machine', 'deal', 'status') 'fields': ('task', 'date', 'machine')
}), }),
('Чертеж и параметры', { ('Исполнение', {
'fields': ('drawing_file', 'drawing_name', 'size_value', 'extra_drawing', 'material', 'quantity_plan', 'is_bend') 'fields': ('quantity_plan', 'quantity_fact', 'status', 'is_synced_1c')
}), }),
('Исполнение (Оператор)', { ('Отходы и материалы', {
'fields': ('operator', 'quantity_fact', 'material_taken', 'usable_waste', 'scrap_weight', 'is_synced_1c') 'fields': ('material_taken', 'usable_waste', 'scrap_weight')
}), }),
) )
def save_model(self, request, obj, form, change): def get_deal(self, obj): return obj.task.deal
""" get_deal.short_description = 'Сделка'
Переопределяем сохранение, чтобы автоматизировать заполнение полей.
"""
# 1. Если имя детали "Б/ч" или пустое, а файл загружен — берем имя из файла
if (obj.drawing_name == "Б/ч" or not obj.drawing_name) and obj.drawing_file:
filename = os.path.basename(obj.drawing_file.name)
obj.drawing_name = os.path.splitext(filename)[0] # Отрезаем .dxf или .step
# 2. Логика запоминания последней сделки (через сессию браузера) def get_drawing(self, obj): return obj.task.drawing_name
if obj.deal: get_drawing.short_description = 'Деталь'
request.session['last_deal_id'] = obj.deal.id
super().save_model(request, obj, form, change)
def get_changeform_initial_data(self, request):
"""
Подставляем последнюю выбранную сделку в новую форму.
"""
initial = super().get_changeform_initial_data(request)
if 'last_deal_id' in request.session:
initial['deal'] = request.session['last_deal_id']
return initial
# Регистрация станков просто списком # Регистрация станков просто списком
admin.site.register(Machine) admin.site.register(Machine)

View File

@@ -0,0 +1,9 @@
from django.conf import settings
def env_info(request):
"""
Прокидываем ENV_TYPE во все шаблоны.
"""
return {
'ENV_TYPE': getattr(settings, 'ENV_TYPE', 'local')
}

View File

@@ -0,0 +1,86 @@
# Generated by Django 6.0.3 on 2026-03-29 12:51
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shiftflow', '0003_employeeprofile'),
]
operations = [
migrations.AlterModelOptions(
name='item',
options={'ordering': ['-date', 'task__deal'], 'verbose_name': 'Позиция сменки', 'verbose_name_plural': 'Реестр сменных заданий'},
),
migrations.RemoveField(
model_name='item',
name='deal',
),
migrations.RemoveField(
model_name='item',
name='drawing_file',
),
migrations.RemoveField(
model_name='item',
name='drawing_name',
),
migrations.RemoveField(
model_name='item',
name='extra_drawing',
),
migrations.RemoveField(
model_name='item',
name='is_bend',
),
migrations.RemoveField(
model_name='item',
name='material',
),
migrations.RemoveField(
model_name='item',
name='operator',
),
migrations.RemoveField(
model_name='item',
name='size_value',
),
migrations.AlterField(
model_name='item',
name='date',
field=models.DateField(default=django.utils.timezone.now, verbose_name='Дата смены'),
),
migrations.AlterField(
model_name='item',
name='quantity_plan',
field=models.PositiveIntegerField(verbose_name='План на смену, шт'),
),
migrations.CreateModel(
name='ProductionTask',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('drawing_name', models.CharField(blank=True, default='Б/ч', max_length=255, verbose_name='Название детали')),
('size_value', models.FloatField(help_text='Длина (мм) или Толщина (мм)', verbose_name='Размер детали')),
('drawing_file', models.FileField(blank=True, null=True, upload_to='drawings/%Y/%m/', verbose_name='Исходник (DXF/IGES)')),
('extra_drawing', models.FileField(blank=True, null=True, upload_to='extra_drawings/%Y/%m/', verbose_name='Доп. чертеж (PDF)')),
('quantity_ordered', models.PositiveIntegerField(verbose_name='Заказано всего, шт')),
('is_bend', models.BooleanField(default=False, verbose_name='Гибка')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('deal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='shiftflow.deal', verbose_name='Сделка')),
('material', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='shiftflow.material', verbose_name='Материал')),
],
options={
'verbose_name': 'Задание на деталь',
'verbose_name_plural': 'Задания на детали',
'ordering': ['-created_at'],
},
),
migrations.AddField(
model_name='item',
name='task',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='shiftflow.productiontask', verbose_name='Задание'),
),
]

View File

@@ -49,10 +49,35 @@ class Material(models.Model):
class Meta: class Meta:
verbose_name = "Материал"; verbose_name_plural = "Материалы" verbose_name = "Материал"; verbose_name_plural = "Материалы"
class ProductionTask(models.Model):
"""
Основание для производства. Определяет ЧТО делать.
Создается технологом или мастером на основе заказа.
"""
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name="Сделка")
drawing_name = models.CharField("Название детали", max_length=255, blank=True, default="Б/ч")
size_value = models.FloatField("Размер детали", help_text="Длина (мм) или Толщина (мм)")
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="Материал")
quantity_ordered = models.PositiveIntegerField("Заказано всего, шт")
is_bend = models.BooleanField("Гибка", default=False)
created_at = models.DateTimeField("Дата создания", auto_now_add=True)
class Meta:
verbose_name = "Задание на деталь"; verbose_name_plural = "Задания на детали"
ordering = ['-created_at']
def __str__(self):
return f"{self.drawing_name} (Заказ {self.deal.number})"
class Item(models.Model): class Item(models.Model):
""" """
Единица сменного задания. Основная рабочая сущность. Единица сменного задания. Определяет КТО, КОГДА и СКОЛЬКО сделал.
Статус по умолчанию 'work' (В работе).
""" """
STATUS_CHOICES = [ STATUS_CHOICES = [
('work', 'В работе'), ('work', 'В работе'),
@@ -61,23 +86,15 @@ class Item(models.Model):
('leftover', 'Недодел'), ('leftover', 'Недодел'),
] ]
# --- База (заполняет начальник) --- # --- Ссылка на основу (временно null=True для миграции старых данных) ---
date = models.DateField("Дата задания", default=timezone.now) task = models.ForeignKey(ProductionTask, on_delete=models.CASCADE, related_name='items', verbose_name="Задание", null=True, blank=True)
# --- Смена (заполняет мастер) ---
date = models.DateField("Дата смены", default=timezone.now)
machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок") machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок")
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name="Сделка") quantity_plan = models.PositiveIntegerField("План на смену, шт")
drawing_name = models.CharField("Название детали", max_length=255, blank=True, default="Б/ч") # --- Исполнение (заполняет оператор) ---
size_value = models.FloatField("Размер детали", help_text="Длина (мм) или Толщина (мм)")
drawing_file = models.FileField("Исходник (DXF/STEP)", 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="Материал")
quantity_plan = models.PositiveIntegerField("План, шт")
is_bend = models.BooleanField("Гибка", default=False)
# --- Исполнение (заполняет оператор/мастер) ---
operator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Исполнитель")
quantity_fact = models.PositiveIntegerField("Факт, шт", default=0) quantity_fact = models.PositiveIntegerField("Факт, шт", default=0)
material_taken = models.TextField("Взятый материал", blank=True, help_text="Напр: 3 трубы по 12м") material_taken = models.TextField("Взятый материал", blank=True, help_text="Напр: 3 трубы по 12м")
@@ -89,11 +106,11 @@ 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', 'deal'] ordering = ['-date', 'task__deal']
def __str__(self): def __str__(self):
return f"{self.drawing_name} ({self.quantity_plan} шт.)" return f"{self.task.drawing_name} - {self.date}"
class EmployeeProfile(models.Model): class EmployeeProfile(models.Model):

View File

@@ -6,8 +6,8 @@
<div class="card shadow-sm border-secondary mb-4"> <div class="card shadow-sm border-secondary mb-4">
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3"> <div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
<h3 class="text-accent mb-0"><i class="bi bi-info-circle me-2"></i>{{ item.drawing_name|default:"Без названия" }}</h3> <h3 class="text-accent mb-0"><i class="bi bi-info-circle me-2"></i>{{ item.task.drawing_name|default:"Без названия" }}</h3>
<span class="badge bg-secondary">Сделка № {{ item.deal.number }}</span> <span class="badge bg-secondary">Сделка № {{ item.task.deal.number }}</span>
</div> </div>
<form method="post" id="mainForm" class="card-body p-4"> <form method="post" id="mainForm" class="card-body p-4">
@@ -20,7 +20,7 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<small class="text-muted d-block">Материал</small> <small class="text-muted d-block">Материал</small>
<strong>{{ item.material.name }}</strong> <strong>{{ item.task.material.name }}</strong>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<small class="text-muted d-block">План</small> <small class="text-muted d-block">План</small>
@@ -29,8 +29,8 @@
</div> </div>
<div class="mb-4 d-flex gap-2"> <div class="mb-4 d-flex gap-2">
{% if item.drawing_file %}<a href="{{ item.drawing_file.url }}" target="_blank" class="btn btn-outline-info btn-sm">DXF</a>{% endif %} {% if item.task.drawing_file %}<a href="{{ item.task.drawing_file.url }}" target="_blank" class="btn btn-outline-info btn-sm">DXF</a>{% endif %}
{% if item.extra_drawing %}<a href="{{ item.extra_drawing.url }}" target="_blank" class="btn btn-outline-danger btn-sm">PDF</a>{% endif %} {% if item.task.extra_drawing %}<a href="{{ item.task.extra_drawing.url }}" target="_blank" class="btn btn-outline-danger btn-sm">PDF</a>{% endif %}
</div> </div>
<input type="hidden" name="status" id="id_status" value="{{ item.status }}"> <input type="hidden" name="status" id="id_status" value="{{ item.status }}">

View File

@@ -22,10 +22,16 @@ class RegistryView(LoginRequiredMixin, ListView):
context_object_name = 'items' context_object_name = 'items'
def get_queryset(self): def get_queryset(self):
# Позже здесь добавим: .filter(machine__in=request.user.profile.machines.all()) # Оптимизируем запросы, подгружая связанные данные сразу
# Сортируем: сначала статус (по алфавиту или логике choices), queryset = Item.objects.select_related('task', 'task__deal', 'task__material', 'machine').all()
# затем по дате (свежие сверху), по станку и по номеру сделки
return Item.objects.all().order_by('status', '-date', 'machine__name', 'deal__number') # Если это оператор, показываем только задания для его станков
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)
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)
@@ -38,10 +44,11 @@ class RegistryView(LoginRequiredMixin, ListView):
class ItemUpdateView(LoginRequiredMixin, UpdateView): class ItemUpdateView(LoginRequiredMixin, UpdateView):
model = Item model = Item
template_name = 'shiftflow/item_detail.html' template_name = 'shiftflow/item_detail.html'
# Перечисляем поля, которые можно редактировать (укажи нужные) # Перечисляем поля, которые можно редактировать в сменке
fields = [ fields = [
'drawing_name', 'machine', 'quantity_plan', 'quantity_fact', 'machine', 'quantity_plan', 'quantity_fact',
'material', 'size_value', 'status', 'is_synced_1c', 'extra_drawing' 'status', 'is_synced_1c',
'material_taken', 'usable_waste', 'scrap_weight'
] ]
context_object_name = 'item' context_object_name = 'item'

View File

@@ -1,8 +1,14 @@
<footer class="footer-custom mt-auto py-3"> <footer class="footer-custom mt-auto py-3 border-top border-secondary">
<div class="container-fluid text-center"> <div class="container-fluid text-center">
<span class="text-muted small"> <div class="text-muted small">Система учета сменных заданий</div>
Система учета сменных заданий, разработана <strong>ACK</strong> &copy; 2026 <div class="text-muted small">
разработана <strong>ACK</strong> &copy; 2026
<i class="bi bi-cpu ms-2 text-accent"></i> <i class="bi bi-cpu ms-2 text-accent"></i>
{% if user.is_superuser %}
<span class="badge bg-dark border border-secondary text-muted fw-normal ms-1" style="font-size: 0.65rem; vertical-align: middle;">
{{ ENV_TYPE|upper }}
</span> </span>
{% endif %}
</div>
</div> </div>
</footer> </footer>