Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

87 changed files with 915 additions and 9 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

14
.idea/backend-fontend.iml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

6
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/barcode/settings.py" charset="UTF-8" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (backend-fontend)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (backend-fontend)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/backend-fontend.iml" filepath="$PROJECT_DIR$/.idea/backend-fontend.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -16,6 +16,9 @@ from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
@ -25,6 +28,10 @@ SECRET_KEY = 'django-insecure-i#8aotin&c00-&#v@x!moruf-uimxr#c!8pi9ehltop98-@bzr
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
if DEBUG: # Убедитесь, что это только для разработки
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [],
}
ALLOWED_HOSTS = []
CELERY_BROKER_URL = 'redis://localhost:6379/0'
@ -44,6 +51,8 @@ INSTALLED_APPS = [
'scan',
'forms',
'users',
'issues',
'production',
"channels",
'rest_framework',
'rest_framework_simplejwt',
@ -99,10 +108,10 @@ WSGI_APPLICATION = 'barcode.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydatabase',
'USER': 'myuser',
'PASSWORD': 'mypassword',
'HOST': '31.130.144.182',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': '1240069630Bk!',
'HOST': 'localhost',
'PORT': '5432',
'OPTIONS': {
'client_encoding': 'UTF8',
@ -167,7 +176,12 @@ REST_FRAMEWORK = {
),
}
CORS_ALLOW_HEADERS = ['Content-Type'] # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
CORS_ALLOW_HEADERS = [
'authorization',
'content-type',
'x-csrftoken',
'x-requested-with',
]
CORS_ALLOW_ALL_ORIGINS = True # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
# <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> localhost:3000

View File

@ -30,4 +30,6 @@ urlpatterns = [
path("inventory/", include("inventory.urls")),
path("forms/", include("forms.urls")),
path("api/users/", include("users.urls")),
path("api/production/", include("production.urls")),
path("reference/", include("issues.urls")),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.5 on 2025-03-06 04:31
# Generated by Django 5.1.5 on 2025-03-21 23:48
from django.db import migrations, models

View File

@ -1,6 +1,7 @@
# Generated by Django 5.1.5 on 2025-03-06 04:31
# Generated by Django 5.1.5 on 2025-03-21 23:48
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@ -9,6 +10,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
@ -44,4 +46,24 @@ class Migration(migrations.Migration):
('sticker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.sticker')),
],
),
migrations.CreateModel(
name='StickerTransferRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('status', models.CharField(choices=[('pending', 'Ожидает'), ('accepted', 'Принята'), ('declined', 'Отклонена'), ('cancelled', 'Отменена')], default='pending', max_length=20)),
('comment', models.TextField(blank=True, null=True)),
('from_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_transfers', to=settings.AUTH_USER_MODEL)),
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_transfers', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='StickerTransferItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.IntegerField()),
('sticker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.sticker')),
('transfer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='inventory.stickertransferrequest')),
],
),
]

View File

@ -1,5 +1,7 @@
from django.db import models
from datetime import timedelta, date
from django.contrib.auth.models import User
class Nomenclature(models.Model):
name = models.CharField(max_length=255)
@ -57,3 +59,23 @@ class StickerMovement(models.Model):
self.sticker.location = "склад"
self.sticker.save()
super().save(*args, **kwargs)
class StickerTransferRequest(models.Model):
STATUS_CHOICES = [
("pending", "Ожидает"),
("accepted", "Принята"),
("declined", "Отклонена"),
("cancelled", "Отменена"),
]
from_user = models.ForeignKey(User, related_name="sent_transfers", on_delete=models.CASCADE)
to_user = models.ForeignKey(User, related_name="received_transfers", on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
comment = models.TextField(blank=True, null=True)
class StickerTransferItem(models.Model):
transfer = models.ForeignKey(StickerTransferRequest, related_name="items", on_delete=models.CASCADE)
sticker = models.ForeignKey(Sticker, on_delete=models.CASCADE)
quantity = models.IntegerField()

View File

@ -1,6 +1,7 @@
from rest_framework import serializers
from .models import Nomenclature, Sticker, StickerMovement
from datetime import date
from django.contrib.auth.models import User
class NomenclatureSerializer(serializers.ModelSerializer):
@ -32,3 +33,35 @@ class StickerMovementSerializer(serializers.ModelSerializer):
class Meta:
model = StickerMovement
fields = '__all__'
""" Transfer sticks """
from rest_framework import serializers
from .models import StickerTransferRequest, StickerTransferItem, Sticker
class StickerTransferItemSerializer(serializers.ModelSerializer):
class Meta:
model = StickerTransferItem
fields = ['id', 'sticker', 'quantity']
class StickerTransferRequestSerializer(serializers.ModelSerializer):
items = StickerTransferItemSerializer(many=True)
from_user = serializers.StringRelatedField(read_only=True)
to_user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())
class Meta:
model = StickerTransferRequest
fields = ['id', 'from_user', 'to_user', 'status', 'created_at', 'items']
def create(self, validated_data):
items_data = validated_data.pop('items')
transfer = StickerTransferRequest.objects.create(**validated_data)
for item in items_data:
StickerTransferItem.objects.create(transfer=transfer, **item)
return transfer

View File

@ -0,0 +1,21 @@
from inventory.models import StickerTransferItem
class StickerTransferItemSerializer(serializers.ModelSerializer):
class Meta:
model = StickerTransferItem
fields = "__all__"
class StickerTransferRequestSerializer(serializers.ModelSerializer):
items = StickerTransferItemSerializer(many=True)
class Meta:
model = StickerTransferRequest
fields = "__all__"
def create(self, validated_data):
items_data = validated_data.pop("items")
transfer = StickerTransferRequest.objects.create(**validated_data)
for item in items_data:
StickerTransferItem.objects.create(transfer=transfer, **item)
return transfer

View File

@ -1,13 +1,35 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import NomenclatureViewSet, StickerViewSet, StickerMovementViewSet, ExternalNomenclatureView
from .views import (
NomenclatureViewSet,
StickerViewSet,
StickerMovementViewSet,
ExternalNomenclatureView,
)
from .views_transfer import StickerTransferRequestViewSet # Импортируем новый ViewSet
router = DefaultRouter()
router.register(r'nomenclature', NomenclatureViewSet)
router.register(r'stickers', StickerViewSet)
router.register(r'movement', StickerMovementViewSet)
router.register(r'transfers', StickerTransferRequestViewSet, basename='stickertransfer') # Добавляем
urlpatterns = [
path('api/', include(router.urls)),
path('api/external-nomenclature/', ExternalNomenclatureView.as_view(), name="external-nomenclature"),
]
"""Трансфер передачи стикеров"""
"""{
"to_user": 3, // <-- ID пользователя, а не имя!
"items": [
{
"sticker": 12, // ID стикера
"quantity": 10
},
{
"sticker": 15,
"quantity": 5
}
]
}
"""

View File

@ -117,6 +117,11 @@ class StickerViewSet(viewsets.ModelViewSet):
self.perform_destroy(instance)
return Response({"message": "Стикер успешно удалён"}, status=status.HTTP_204_NO_CONTENT)
"""Передача стикеров"""
class StickerMovementViewSet(viewsets.ModelViewSet):
queryset = StickerMovement.objects.all()
serializer_class = StickerMovementSerializer

View File

@ -0,0 +1,58 @@
from rest_framework import viewsets, status
from rest_framework.response import Response
from rest_framework.decorators import action
from django.contrib.auth.models import User
from .models import StickerTransferRequest, StickerTransferItem, Sticker, StickerMovement
from .serializers import StickerTransferRequestSerializer, StickerTransferItemSerializer
from django.shortcuts import get_object_or_404
from django.utils import timezone
class StickerTransferRequestViewSet(viewsets.ModelViewSet):
queryset = StickerTransferRequest.objects.all().select_related('from_user', 'to_user').prefetch_related('items__sticker')
serializer_class = StickerTransferRequestSerializer
def get_queryset(self):
user = self.request.user
return StickerTransferRequest.objects.filter(from_user=user) | StickerTransferRequest.objects.filter(to_user=user)
def perform_create(self, serializer):
serializer.save(from_user=self.request.user)
@action(detail=True, methods=['post'])
def accept(self, request, pk=None):
transfer = get_object_or_404(StickerTransferRequest, pk=pk)
if transfer.to_user != request.user:
return Response({"error": "Нет прав для принятия этой передачи."}, status=status.HTTP_403_FORBIDDEN)
if transfer.status != 'pending':
return Response({"error": "Передача уже обработана."}, status=status.HTTP_400_BAD_REQUEST)
# Перемещение стикеров
for item in transfer.items.all():
StickerMovement.objects.create(
sticker=item.sticker,
from_location='склад',
to_location='цех',
quantity=item.quantity
)
item.sticker.location = 'цех'
item.sticker.save()
transfer.status = 'accepted'
transfer.save()
return Response({"status": "Принята"})
@action(detail=True, methods=['post'])
def decline(self, request, pk=None):
transfer = get_object_or_404(StickerTransferRequest, pk=pk)
if transfer.to_user != request.user:
return Response({"error": "Нет прав для отклонения этой передачи."}, status=status.HTTP_403_FORBIDDEN)
if transfer.status != 'pending':
return Response({"error": "Передача уже обработана."}, status=status.HTTP_400_BAD_REQUEST)
transfer.status = 'declined'
transfer.save()
return Response({"status": "Отклонена"})

0
issues/__init__.py Normal file
View File

16
issues/admin.py Normal file
View File

@ -0,0 +1,16 @@
from django.contrib import admin
from .models import IssueCategory, Zone, Issue
@admin.register(IssueCategory)
class IssueCategoryAdmin(admin.ModelAdmin):
list_display = ("name",)
@admin.register(Zone)
class ZoneAdmin(admin.ModelAdmin):
list_display = ("name",)
@admin.register(Issue)
class IssueAdmin(admin.ModelAdmin):
list_display = ("category", "zone", "description", "created_at")
list_filter = ("category", "zone")
search_fields = ("description",)

6
issues/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class IssuesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'issues'

View File

@ -0,0 +1,59 @@
# Generated by Django 5.1.5 on 2025-03-23 07:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='IssueCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=100, unique=True)),
('text', models.TextField()),
],
),
migrations.CreateModel(
name='Nomenclature',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('short_name', models.CharField(max_length=100)),
('gtin', models.CharField(max_length=14, unique=True)),
('groupe_water', models.CharField(max_length=255)),
('groupe_names', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='Zone',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
),
migrations.CreateModel(
name='Issue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='issues.issuecategory')),
('zone', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='issues.zone')),
],
),
]

View File

93
issues/models.py Normal file
View File

@ -0,0 +1,93 @@
from django.db import models
class IssueCategory(models.Model):
"""Категории проблем"""
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name
class Zone(models.Model):
"""Зоны, где возникают проблемы"""
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name
class Issue(models.Model):
"""Конкретные проблемы"""
category = models.ForeignKey(IssueCategory, on_delete=models.CASCADE, related_name="issues")
zone = models.ForeignKey(Zone, on_delete=models.CASCADE, related_name="issues")
description = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.category.name} - {self.zone.name}: {self.description[:50]}"
"""GET /api/categories/ список категорий
POST /api/categories/ создать категорию
GET /api/zones/ список зон
POST /api/zones/ создать зону
GET /api/issues/ список проблем
POST /api/issues/ добавить проблему
DELETE /api/issues/<id>/ удалить проблему"""
"""Модели справочника номенклатура и сообщения"""
class Nomenclature(models.Model):
name = models.CharField(max_length=255)
short_name = models.CharField(max_length=100)
gtin = models.CharField(max_length=14, unique=True)
groupe_water = models.CharField(max_length=255)
groupe_names = models.CharField(max_length=255)
def __str__(self):
return self.name
class Message(models.Model):
identifier = models.CharField(max_length=100, unique=True)
text = models.TextField()
def __str__(self):
return f"{self.identifier}: {self.text[:30]}"
class Plant(models.Model):
"""Справочник заводов"""
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name
class ProductionLine(models.Model):
"""Справочник производственных линий"""
name = models.CharField(max_length=255, unique=True)
plant = models.ForeignKey(Plant, on_delete=models.CASCADE, related_name="lines")
def __str__(self):
return f"{self.plant.name} - {self.name}"
class ProductionZone(models.Model):
"""Справочник производственных зон"""
name = models.CharField(max_length=255)
line = models.ForeignKey(ProductionLine, on_delete=models.CASCADE, related_name="zones")
class Meta:
unique_together = ('name', 'line') # Зона уникальна в пределах линии
def __str__(self):
return f"{self.line.name} - {self.name}"
class DowntimeReason(models.Model):
"""Справочник причин простоев"""
name = models.CharField(max_length=255, unique=True)
description = models.TextField(blank=True)
def __str__(self):
return self.name

33
issues/serializers.py Normal file
View File

@ -0,0 +1,33 @@
from rest_framework import serializers
from .models import IssueCategory, Zone, Issue, Nomenclature, Message
class IssueCategorySerializer(serializers.ModelSerializer):
class Meta:
model = IssueCategory
fields = '__all__'
class ZoneSerializer(serializers.ModelSerializer):
class Meta:
model = Zone
fields = '__all__'
class IssueSerializer(serializers.ModelSerializer):
category = IssueCategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(queryset=IssueCategory.objects.all(), source='category', write_only=True)
zone = ZoneSerializer(read_only=True)
zone_id = serializers.PrimaryKeyRelatedField(queryset=Zone.objects.all(), source='zone', write_only=True)
class Meta:
model = Issue
fields = ['id', 'category', 'category_id', 'zone', 'zone_id', 'description', 'created_at', 'updated_at']
class NomenclatureSerializer(serializers.ModelSerializer):
class Meta:
model = Nomenclature
fields = '__all__'
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = '__all__'

3
issues/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
issues/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from inventory.views import NomenclatureViewSet
from .serializers import NomenclatureSerializer
from .views import IssueCategoryViewSet, ZoneViewSet, IssueViewSet, NomenclatureViewSet, MessageViewSet
router = DefaultRouter()
router.register(r'categories', IssueCategoryViewSet)
router.register(r'zones', ZoneViewSet)
router.register(r'issues', IssueViewSet)
router.register(r'nomenclature', NomenclatureViewSet)
router.register(r'messages', MessageViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]

29
issues/views.py Normal file
View File

@ -0,0 +1,29 @@
from rest_framework import viewsets
from .models import IssueCategory, Zone, Issue
from .serializers import IssueCategorySerializer, ZoneSerializer, IssueSerializer
class IssueCategoryViewSet(viewsets.ModelViewSet):
queryset = IssueCategory.objects.all()
serializer_class = IssueCategorySerializer
class ZoneViewSet(viewsets.ModelViewSet):
queryset = Zone.objects.all()
serializer_class = ZoneSerializer
class IssueViewSet(viewsets.ModelViewSet):
queryset = Issue.objects.all().order_by('-created_at')
serializer_class = IssueSerializer
"""Справочник номенклатуры и сообщений"""
from rest_framework import viewsets
from .models import Nomenclature, Message
from .serializers import NomenclatureSerializer, MessageSerializer
class NomenclatureViewSet(viewsets.ModelViewSet):
queryset = Nomenclature.objects.all()
serializer_class = NomenclatureSerializer
class MessageViewSet(viewsets.ModelViewSet):
queryset = Message.objects.all()
serializer_class = MessageSerializer

321
mobileApps/fletFormTest.py Normal file
View File

@ -0,0 +1,321 @@
import flet as ft
from datetime import datetime
def main(page: ft.Page):
# Состояние формы
state = {
"current_step": 0,
"operators_name": "",
"factory": "",
"line": "",
"error_zone": "",
"problems": set(), # будем накапливать выбранные значения
"downtime_reasons": set(),
"custom_problem": "",
"custom_reason": "",
"fix_method": "",
"start_at": "",
"end_at": "",
# Зависимые опции:
"line_options": [],
"zone_options": [],
"problem_options": [],
"reason_options": [],
}
# Справочники (аналогичные примерам в React/Ionic)
factory_line_map = {
"Славда": ["ПЭТ", "19л", ""],
"Скит": ["Sipa", "Devin", "JR", "19 Литров", "5 Литров"]
}
line_zone_map = {
"Sipa": ["Сериализация", "Упаковка", "Сборка палеты", "Закрытие палеты"],
"Devin": ["Сериализация", "Упаковка", "Сборка палеты"],
"JR": ["Сериализация", "Упаковка", "Сборка палеты", "Закрытие палеты"],
"19 Литров": ["Валидация", "Тонкий клиент"],
"5 Литров": ["Валидация", "Тонкий клиент"],
"ПЭТ": ["Сериализация", "Упаковка", "Сборка палеты", "Закрытие палеты"],
"": ["Сериализация", "Упаковка", "Сборка палеты", "Закрытие палеты"],
"19л": ["Валидация", "Тонкий клиент"],
}
common_problems = {
"Славда": ["Не печатает код", "Камера не считывает код", "Сбой принтера"],
"Скит": ["Не печатает код", "Камера не считывает код", "Сбой принтера"],
}
common_downtime_reasons = {
"Славда": ["Сбой камеры", "Ошибка валидации", "Проблема с печатью"],
"Скит": ["Сбой камеры", "Ошибка валидации", "Проблема с печатью"],
}
# Элементы UI, которые будут обновляться
operators_name_field = ft.TextField(label="Имя оператора *", width=300)
factory_dropdown = ft.Dropdown(
label="Завод *",
options=[
ft.dropdown.Option("Славда"),
ft.dropdown.Option("Скит")
],
width=300
)
line_dropdown = ft.Dropdown(label="Линия *", width=300)
zone_dropdown = ft.Dropdown(label="Зона *", width=300)
fix_method_field = ft.TextField(label="Как решили проблему? *", width=300)
start_at_field = ft.TextField(label="Начало простоя (YYYY-MM-DD HH:MM) *", width=300)
end_at_field = ft.TextField(label="Окончание простоя (YYYY-MM-DD HH:MM) *", width=300)
custom_problem_field = ft.TextField(label="Своя проблема (необязательно)", width=300)
custom_reason_field = ft.TextField(label="Своя причина (необязательно)", width=300)
# Для выбора нескольких вариантов используем колонки с чекбоксами
problems_checkboxes = ft.Column()
reasons_checkboxes = ft.Column()
# Функция для показа уведомлений (SnackBar)
def show_snackbar(message, bgcolor):
page.snack_bar = ft.SnackBar(ft.Text(message), bgcolor=bgcolor)
page.snack_bar.open = True
page.update()
# Обновление зависимых списков при выборе завода
def update_factory_dependent():
factory = state["factory"]
state["line_options"] = factory_line_map.get(factory, [])
line_dropdown.options = [ft.dropdown.Option(opt) for opt in state["line_options"]]
line_dropdown.value = None
state["line"] = ""
state["problem_options"] = common_problems.get(factory, [])
state["reason_options"] = common_downtime_reasons.get(factory, [])
update_problems_checkboxes()
update_reasons_checkboxes()
page.update()
# Обновление списка зон при выборе линии
def update_line_dependent():
line = state["line"]
state["zone_options"] = line_zone_map.get(line, [])
zone_dropdown.options = [ft.dropdown.Option(opt) for opt in state["zone_options"]]
zone_dropdown.value = None
state["error_zone"] = ""
page.update()
def update_problems_checkboxes():
problems_checkboxes.controls.clear()
for prob in state["problem_options"]:
chk = ft.Checkbox(label=prob, value=False)
def on_change(e, label=prob):
if e.control.value:
state["problems"].add(label)
else:
state["problems"].discard(label)
chk.on_change = on_change
problems_checkboxes.controls.append(chk)
page.update()
def update_reasons_checkboxes():
reasons_checkboxes.controls.clear()
for reason in state["reason_options"]:
chk = ft.Checkbox(label=reason, value=False)
def on_change(e, label=reason):
if e.control.value:
state["downtime_reasons"].add(label)
else:
state["downtime_reasons"].discard(label)
chk.on_change = on_change
reasons_checkboxes.controls.append(chk)
page.update()
# Функция валидации этапов
def validate_step():
step = state["current_step"]
if step == 0:
if not operators_name_field.value.strip() or not factory_dropdown.value:
show_snackbar("Введите имя и выберите завод", ft.colors.RED)
return False
elif step == 1:
if not line_dropdown.value or not zone_dropdown.value:
show_snackbar("Выберите линию и зону", ft.colors.RED)
return False
elif step == 2:
total_problems = set(state["problems"])
if custom_problem_field.value and custom_problem_field.value.strip():
total_problems.add(custom_problem_field.value.strip())
total_reasons = set(state["downtime_reasons"])
if custom_reason_field.value and custom_reason_field.value.strip():
total_reasons.add(custom_reason_field.value.strip())
if not total_problems:
show_snackbar("Укажите хотя бы одну проблему", ft.colors.RED)
return False
if not total_reasons:
show_snackbar("Укажите хотя бы одну причину простоя", ft.colors.RED)
return False
elif step == 3:
if not fix_method_field.value.strip():
show_snackbar("Опишите, как решили проблему", ft.colors.RED)
return False
if not start_at_field.value.strip() or not end_at_field.value.strip():
show_snackbar("Укажите время начала и окончания", ft.colors.RED)
return False
try:
datetime.strptime(start_at_field.value.strip(), "%Y-%m-%d %H:%M")
datetime.strptime(end_at_field.value.strip(), "%Y-%m-%d %H:%M")
except ValueError:
show_snackbar("Неверный формат даты/времени", ft.colors.RED)
return False
return True
# Контейнер для отображения содержимого этапа
content_column = ft.Column()
# Прогресс-бар (значение от 0 до 1)
progress_bar = ft.ProgressBar(value=(state["current_step"]+1)/4)
# Кнопки навигации
next_button = ft.ElevatedButton(text="Далее")
back_button = ft.ElevatedButton(text="Назад")
submit_button = ft.ElevatedButton(text="Отправить")
# Обновление UI в зависимости от этапа
def update_ui():
content_column.controls.clear()
progress_bar.value = (state["current_step"]+1)/4
if state["current_step"] == 0:
content_column.controls.extend([
operators_name_field,
factory_dropdown,
])
elif state["current_step"] == 1:
content_column.controls.extend([
line_dropdown,
zone_dropdown,
])
elif state["current_step"] == 2:
content_column.controls.extend([
ft.Text("Проблемы (можно выбрать несколько):"),
problems_checkboxes,
custom_problem_field,
ft.Text("Причины простоя (можно выбрать несколько):"),
reasons_checkboxes,
custom_reason_field,
])
elif state["current_step"] == 3:
content_column.controls.extend([
fix_method_field,
start_at_field,
end_at_field,
])
# Кнопки навигации
nav_buttons = []
if state["current_step"] > 0:
nav_buttons.append(back_button)
if state["current_step"] < 3:
nav_buttons.append(next_button)
if state["current_step"] == 3:
nav_buttons.append(submit_button)
content_column.controls.append(ft.Row(controls=nav_buttons, spacing=20))
page.update()
# Обработчики кнопок
def on_next(e):
if validate_step():
if state["current_step"] == 0:
state["operators_name"] = operators_name_field.value
state["factory"] = factory_dropdown.value
update_factory_dependent()
elif state["current_step"] == 1:
state["line"] = line_dropdown.value
state["error_zone"] = zone_dropdown.value
state["current_step"] += 1
update_ui()
def on_back(e):
state["current_step"] -= 1
update_ui()
def on_submit(e):
if not validate_step():
return
state["fix_method"] = fix_method_field.value
state["start_at"] = start_at_field.value
state["end_at"] = end_at_field.value
total_problems = list(state["problems"])
if custom_problem_field.value and custom_problem_field.value.strip():
total_problems.append(custom_problem_field.value.strip())
total_reasons = list(state["downtime_reasons"])
if custom_reason_field.value and custom_reason_field.value.strip():
total_reasons.append(custom_reason_field.value.strip())
payload = {
"factory": state["factory"],
"line": state["line"],
"operators_name": state["operators_name"],
"error_zone": state["error_zone"],
"problem": ", ".join(total_problems),
"downtime_reason": ", ".join(total_reasons),
"fix_method": state["fix_method"],
"start_at": state["start_at"],
"end_at": state["end_at"],
}
print("[DEBUG] Отправляем JSON:", payload)
show_snackbar("Данные успешно отправлены!", ft.colors.GREEN)
# Сброс формы
state.update({
"current_step": 0,
"operators_name": "",
"factory": "",
"line": "",
"error_zone": "",
"problems": set(),
"downtime_reasons": set(),
"custom_problem": "",
"custom_reason": "",
"fix_method": "",
"start_at": "",
"end_at": "",
"line_options": [],
"zone_options": [],
"problem_options": [],
"reason_options": [],
})
operators_name_field.value = ""
factory_dropdown.value = None
line_dropdown.value = None
zone_dropdown.value = None
fix_method_field.value = ""
start_at_field.value = ""
end_at_field.value = ""
custom_problem_field.value = ""
custom_reason_field.value = ""
problems_checkboxes.controls.clear()
reasons_checkboxes.controls.clear()
update_ui()
next_button.on_click = on_next
back_button.on_click = on_back
submit_button.on_click = on_submit
def on_factory_change(e):
state["factory"] = factory_dropdown.value
update_factory_dependent()
factory_dropdown.on_change = on_factory_change
def on_line_change(e):
state["line"] = line_dropdown.value
update_line_dependent()
line_dropdown.on_change = on_line_change
def on_zone_change(e):
state["error_zone"] = zone_dropdown.value
zone_dropdown.on_change = on_zone_change
page.title = "Журнал простоев (мобильная версия) - Flet"
page.add(progress_bar, content_column)
update_ui()
ft.app(target=main)

0
production/__init__.py Normal file
View File

3
production/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
production/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ProductionConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'production'

View File

@ -0,0 +1,27 @@
# Generated by Django 5.1.5 on 2025-03-21 23:48
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ProductionPlan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(verbose_name='Дата производства')),
('line', models.CharField(max_length=100, verbose_name='Линия')),
('product', models.CharField(max_length=100, verbose_name='Продукция')),
('production', models.PositiveIntegerField(verbose_name='Плановый выпуск продукции')),
('shift', models.CharField(max_length=50, verbose_name='Смена')),
('employee', models.CharField(max_length=100, verbose_name='Сотрудник')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
],
),
]

View File

13
production/models.py Normal file
View File

@ -0,0 +1,13 @@
from django.db import models
class ProductionPlan(models.Model):
date = models.DateField(verbose_name="Дата производства")
line = models.CharField(max_length=100, verbose_name="Линия")
product = models.CharField(max_length=100, verbose_name="Продукция")
production = models.PositiveIntegerField(verbose_name="Плановый выпуск продукции")
shift = models.CharField(max_length=50, verbose_name="Смена")
employee = models.CharField(max_length=100, verbose_name="Сотрудник")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создано")
def __str__(self):
return f"{self.date} - {self.line} - {self.product}"

View File

@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import ProductionPlan
class ProductionPlanSerializer(serializers.ModelSerializer):
class Meta:
model = ProductionPlan
fields = '__all__'

3
production/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
production/urls.py Normal file
View File

@ -0,0 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProductionPlanViewSet
router = DefaultRouter()
router.register(r'production', ProductionPlanViewSet, basename='production')
urlpatterns = [
path('calendar/', include(router.urls)),
]

7
production/views.py Normal file
View File

@ -0,0 +1,7 @@
from rest_framework import viewsets
from .models import ProductionPlan
from .serializers import ProductionPlanSerializer
class ProductionPlanViewSet(viewsets.ModelViewSet):
queryset = ProductionPlan.objects.all().order_by('-created_at')
serializer_class = ProductionPlanSerializer

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.5 on 2025-03-06 04:31
# Generated by Django 5.1.5 on 2025-03-21 23:48
from django.db import migrations, models

View File

@ -5,8 +5,12 @@ from rest_framework_simplejwt.tokens import RefreshToken
class UserLoginSerializer(serializers.Serializer):
username = serializers.CharField()
profile_name = serializers.SerializerMethodField() # Добавляем поле для профиля
password = serializers.CharField(write_only=True)
def get_profile_name(self, user):
return f"{user.first_name} {user.last_name}".strip() or user.username # Если имя пустое, используем username
def validate(self, data):
from django.contrib.auth import authenticate
@ -18,6 +22,7 @@ class UserLoginSerializer(serializers.Serializer):
return {
"username": user.username,
"profile": self.get_profile_name(user), # Возвращаем профильное имя
"access": str(refresh.access_token),
"refresh": str(refresh)
}