From 191d06d7d3522dcb0277660d5d5f32dedcaec839 Mon Sep 17 00:00:00 2001 From: ackFromRedmi Date: Sun, 29 Mar 2026 16:04:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BC=D0=B5=D0=BD=D1=8F=D0=BB=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D1=83=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- README.md | 7 +- core/settings.py | 1 + docker-compose.yml | 1 + shiftflow/admin.py | 63 ++++++-------- shiftflow/context_processors.py | 9 ++ ..._item_options_remove_item_deal_and_more.py | 86 +++++++++++++++++++ shiftflow/models.py | 57 +++++++----- .../templates/shiftflow/item_detail.html | 10 +-- shiftflow/views.py | 21 +++-- templates/components/_footer.html | 14 ++- 11 files changed, 194 insertions(+), 77 deletions(-) create mode 100644 shiftflow/context_processors.py create mode 100644 shiftflow/migrations/0004_alter_item_options_remove_item_deal_and_more.py diff --git a/.env b/.env index 235bd06..70cfb82 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ DB_USER=prodman_user DB_PASS=prodman_password_zwE45t! # Настройки Django -ENV_TYPE=dev +# ENV_TYPE=dev DB_HOST=db SECRET_KEY='django-insecure-9p*t_(vtwo144=u)ie8qzb31a8%i(&1w4$0vq#udautexj^vms' # todo потом установить домен для продакшена diff --git a/README.md b/README.md index 5b56cbb..d9bee88 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ Если внес изменения в проект и готов отправить их на сервер `ProdServ`: 1. **Подготовь файлы**: - `git add .` + git add . + 2. **Запечатай изменения**: - `git commit -m "Подправил докерфайл сборки контейнера"` + git commit -m "Подправил докерфайл сборки контейнера" 3. **Отправляй в полет**: - `git push origin main` + git push origin main ### 💊 Таблетка: Если не пушится (сброс авторизации) Если Git "забыл" пароль или выдает ошибку Permission Denied: diff --git a/core/settings.py b/core/settings.py index b9c6e1a..2320a87 100644 --- a/core/settings.py +++ b/core/settings.py @@ -89,6 +89,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'shiftflow.context_processors.env_info', # Правильный путь ], }, }, diff --git a/docker-compose.yml b/docker-compose.yml index 389b71b..4b9eee2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: restart: unless-stopped env_file: - .env # Прокидывает все секреты и настройки внутрь Python + - ENV_TYPE=server volumes: # Общие папки для статики и картинок. Сюда Django их складывает. - staticfiles:/app/staticfiles diff --git a/shiftflow/admin.py b/shiftflow/admin.py index cba828b..2862820 100644 --- a/shiftflow/admin.py +++ b/shiftflow/admin.py @@ -1,6 +1,6 @@ import os 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) @@ -20,52 +20,41 @@ class DealAdmin(admin.ModelAdmin): class MaterialAdmin(admin.ModelAdmin): 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) class ItemAdmin(admin.ModelAdmin): - # Что видим в общем списке - list_display = ('date', 'machine', 'deal', 'drawing_name', 'quantity_plan', 'quantity_fact', 'status', 'is_synced_1c') + # Что видим в общем списке (используем task__ для доступа к полям базы) + 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') - # Поиск по номеру сделки и названию детали - search_fields = ('drawing_name', 'deal__number') + list_filter = ('date', 'machine', 'status', 'is_synced_1c', 'task__deal') + # Поиск по номеру сделки и названию детали через связь task + search_fields = ('task__drawing_name', 'task__deal__number') - # Группируем поля в форме редактирования для удобства + # Группируем поля в форме редактирования 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): - """ - Переопределяем сохранение, чтобы автоматизировать заполнение полей. - """ - # 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. Логика запоминания последней сделки (через сессию браузера) - if obj.deal: - 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 + def get_deal(self, obj): return obj.task.deal + get_deal.short_description = 'Сделка' + + def get_drawing(self, obj): return obj.task.drawing_name + get_drawing.short_description = 'Деталь' # Регистрация станков просто списком admin.site.register(Machine) diff --git a/shiftflow/context_processors.py b/shiftflow/context_processors.py new file mode 100644 index 0000000..4f1cbce --- /dev/null +++ b/shiftflow/context_processors.py @@ -0,0 +1,9 @@ +from django.conf import settings + +def env_info(request): + """ + Прокидываем ENV_TYPE во все шаблоны. + """ + return { + 'ENV_TYPE': getattr(settings, 'ENV_TYPE', 'local') + } \ No newline at end of file diff --git a/shiftflow/migrations/0004_alter_item_options_remove_item_deal_and_more.py b/shiftflow/migrations/0004_alter_item_options_remove_item_deal_and_more.py new file mode 100644 index 0000000..e63d6c4 --- /dev/null +++ b/shiftflow/migrations/0004_alter_item_options_remove_item_deal_and_more.py @@ -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='Задание'), + ), + ] diff --git a/shiftflow/models.py b/shiftflow/models.py index b6b76c3..efa52b5 100644 --- a/shiftflow/models.py +++ b/shiftflow/models.py @@ -49,10 +49,35 @@ class Material(models.Model): class Meta: 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): """ - Единица сменного задания. Основная рабочая сущность. - Статус по умолчанию 'work' (В работе). + Единица сменного задания. Определяет КТО, КОГДА и СКОЛЬКО сделал. """ STATUS_CHOICES = [ ('work', 'В работе'), @@ -61,23 +86,15 @@ class Item(models.Model): ('leftover', 'Недодел'), ] - # --- База (заполняет начальник) --- - date = models.DateField("Дата задания", default=timezone.now) + # --- Ссылка на основу (временно null=True для миграции старых данных) --- + 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="Станок") - 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) 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) class Meta: - verbose_name = "Позиция"; verbose_name_plural = "Сменное задание" - ordering = ['-date', 'deal'] + verbose_name = "Позиция сменки"; verbose_name_plural = "Реестр сменных заданий" + ordering = ['-date', 'task__deal'] def __str__(self): - return f"{self.drawing_name} ({self.quantity_plan} шт.)" + return f"{self.task.drawing_name} - {self.date}" class EmployeeProfile(models.Model): diff --git a/shiftflow/templates/shiftflow/item_detail.html b/shiftflow/templates/shiftflow/item_detail.html index da3a86c..f115074 100644 --- a/shiftflow/templates/shiftflow/item_detail.html +++ b/shiftflow/templates/shiftflow/item_detail.html @@ -6,8 +6,8 @@
-

{{ item.drawing_name|default:"Без названия" }}

- Сделка № {{ item.deal.number }} +

{{ item.task.drawing_name|default:"Без названия" }}

+ Сделка № {{ item.task.deal.number }}
@@ -20,7 +20,7 @@
Материал - {{ item.material.name }} + {{ item.task.material.name }}
План @@ -29,8 +29,8 @@
- {% if item.drawing_file %}DXF{% endif %} - {% if item.extra_drawing %}PDF{% endif %} + {% if item.task.drawing_file %}DXF{% endif %} + {% if item.task.extra_drawing %}PDF{% endif %}
diff --git a/shiftflow/views.py b/shiftflow/views.py index 4731e9b..2c51981 100644 --- a/shiftflow/views.py +++ b/shiftflow/views.py @@ -22,10 +22,16 @@ class RegistryView(LoginRequiredMixin, ListView): context_object_name = 'items' def get_queryset(self): - # Позже здесь добавим: .filter(machine__in=request.user.profile.machines.all()) - # Сортируем: сначала статус (по алфавиту или логике choices), - # затем по дате (свежие сверху), по станку и по номеру сделки - return Item.objects.all().order_by('status', '-date', 'machine__name', 'deal__number') + # Оптимизируем запросы, подгружая связанные данные сразу + 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) + + return queryset.order_by('status', '-date', 'machine__name', 'task__deal__number') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -38,10 +44,11 @@ class RegistryView(LoginRequiredMixin, ListView): class ItemUpdateView(LoginRequiredMixin, UpdateView): model = Item template_name = 'shiftflow/item_detail.html' - # Перечисляем поля, которые можно редактировать (укажи нужные) + # Перечисляем поля, которые можно редактировать в сменке fields = [ - 'drawing_name', 'machine', 'quantity_plan', 'quantity_fact', - 'material', 'size_value', 'status', 'is_synced_1c', 'extra_drawing' + 'machine', 'quantity_plan', 'quantity_fact', + 'status', 'is_synced_1c', + 'material_taken', 'usable_waste', 'scrap_weight' ] context_object_name = 'item' diff --git a/templates/components/_footer.html b/templates/components/_footer.html index 5e6a20c..010ef4c 100644 --- a/templates/components/_footer.html +++ b/templates/components/_footer.html @@ -1,8 +1,14 @@ -