Добавил справочники
This commit is contained in:
commit
adc0c14278
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
14
.idea/backend-fontend.iml
generated
Normal file
14
.idea/backend-fontend.iml
generated
Normal 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
6
.idea/encodings.xml
generated
Normal 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>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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>
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Используем официальный образ Python
|
||||||
|
FROM python:3.10
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию в контейнере
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем файлы проекта в контейнер
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Применяем миграции
|
||||||
|
RUN python manage.py migrate
|
||||||
|
|
||||||
|
# Запускаем сервер Django
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .celery_app import app as celery_app
|
||||||
1
barcode/__init__.py
Normal file
1
barcode/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .celery_app import app as celery_app
|
||||||
20
barcode/asgi.py
Normal file
20
barcode/asgi.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for barcode project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from barcode.routing import websocket_urlpatterns
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "barcode.settings")
|
||||||
|
|
||||||
|
application = ProtocolTypeRouter({
|
||||||
|
"http": get_asgi_application(),
|
||||||
|
"websocket": URLRouter(websocket_urlpatterns),
|
||||||
|
})
|
||||||
11
barcode/celery_app.py
Normal file
11
barcode/celery_app.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
# Óęŕçűâŕĺě ďóňü ę íŕńňđîéęŕě Django
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "barcode.settings")
|
||||||
|
|
||||||
|
app = Celery("barcode")
|
||||||
|
|
||||||
|
# Çŕăđóćŕĺě íŕńňđîéęč Celery čç Django
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
20
barcode/consumers.py
Normal file
20
barcode/consumers.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
from .data_fetcher import fetch_lines_data # Вынесем логику получения данных в отдельный модуль
|
||||||
|
|
||||||
|
class LineDataConsumer(AsyncWebsocketConsumer):
|
||||||
|
async def connect(self):
|
||||||
|
await self.accept()
|
||||||
|
self.running = True
|
||||||
|
await self.send(text_data=json.dumps({"message": "WebSocket connected"}))
|
||||||
|
asyncio.create_task(self.send_data_periodically())
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
async def send_data_periodically(self):
|
||||||
|
while self.running:
|
||||||
|
data = await fetch_lines_data()
|
||||||
|
await self.send(text_data=json.dumps(data))
|
||||||
|
await asyncio.sleep(5) # Ждем 5 секунд перед следующим запросом
|
||||||
63
barcode/data_fetcher.py
Normal file
63
barcode/data_fetcher.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
LOGIN_URL_ASPU = "http://192.168.254.10:3428/api/login"
|
||||||
|
|
||||||
|
API_URLS = {
|
||||||
|
"Sipa": "http://192.168.254.10:3428/api/Management/",
|
||||||
|
"Devin": "http://192.168.254.20:3428/api/Management/",
|
||||||
|
"JR": "http://192.168.254.30:3428/api/Management/",
|
||||||
|
"5L": "http://192.168.254.40:3428/api/Management/",
|
||||||
|
"19L": "http://192.168.254.50:3428/api/Management/",
|
||||||
|
}
|
||||||
|
|
||||||
|
USERNAME = "superuser"
|
||||||
|
PASSWORD = "Superuser1105"
|
||||||
|
|
||||||
|
token_cache = {"token": None, "timestamp": 0}
|
||||||
|
TOKEN_LIFETIME = 1440 * 60 # 24 часа в секундах
|
||||||
|
|
||||||
|
def get_new_token():
|
||||||
|
"""Получение нового токена с кешированием."""
|
||||||
|
global token_cache
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if token_cache["token"] and (current_time - token_cache["timestamp"] < TOKEN_LIFETIME):
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
payload = {"UserName": USERNAME, "Password": PASSWORD}
|
||||||
|
try:
|
||||||
|
response = requests.post(LOGIN_URL_ASPU, json=payload, timeout=5)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and response_data.get("IsSuccess"):
|
||||||
|
token_cache["token"] = response_data["Value"]["Token"]
|
||||||
|
token_cache["timestamp"] = current_time
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
logging.error(f"Ошибка получения токена: {response_data}")
|
||||||
|
return None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка сети при получении токена: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def fetch_lines_data():
|
||||||
|
"""Асинхронное получение данных с линий."""
|
||||||
|
token = get_new_token()
|
||||||
|
if not token:
|
||||||
|
return {"error": "Не удалось получить токен"}
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for key, url in API_URLS.items():
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers, timeout=5)
|
||||||
|
response.raise_for_status()
|
||||||
|
results[key] = response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка запроса к {key}: {str(e)}")
|
||||||
|
results[key] = {"error": str(e)}
|
||||||
|
|
||||||
|
return results
|
||||||
6
barcode/routing.py
Normal file
6
barcode/routing.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import re_path
|
||||||
|
from .consumers import LineDataConsumer
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
re_path(r"ws/lines/$", LineDataConsumer.as_asgi()),
|
||||||
|
]
|
||||||
217
barcode/settings.py
Normal file
217
barcode/settings.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
"""
|
||||||
|
Django settings for barcode project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.1.5.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
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/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
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'
|
||||||
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
|
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
|
||||||
|
|
||||||
|
|
||||||
|
CELERY_WORKER_POOL = 'solo'
|
||||||
|
CELERY_WORKER_CONCURRENCY = 1
|
||||||
|
CELERY_TASK_ALWAYS_EAGER = True
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'inventory',
|
||||||
|
'batches',
|
||||||
|
'scan',
|
||||||
|
'forms',
|
||||||
|
'users',
|
||||||
|
'issues',
|
||||||
|
'production',
|
||||||
|
"channels",
|
||||||
|
'rest_framework',
|
||||||
|
'rest_framework_simplejwt',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'corsheaders',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'barcode.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'barcode.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': 'postgres',
|
||||||
|
'USER': 'postgres',
|
||||||
|
'PASSWORD': '1240069630Bk!',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '5432',
|
||||||
|
'OPTIONS': {
|
||||||
|
'client_encoding': 'UTF8',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> UTF-8 <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer', # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JSON
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:3000"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(days=1), # Токен живет 1 день
|
||||||
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh-токен живет 7 дней
|
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',), # Токен передается через заголовок Authorization
|
||||||
|
}
|
||||||
|
|
||||||
|
ASGI_APPLICATION = "your_project.asgi.application"
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "channels.layers.InMemoryChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"capacity": 1000, # Увеличь лимит соединений
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
35
barcode/urls.py
Normal file
35
barcode/urls.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for barcode project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
# from django.contrib import admin
|
||||||
|
# from django.urls import path
|
||||||
|
#
|
||||||
|
# urlpatterns = [
|
||||||
|
# path('admin/', admin.site.urls),
|
||||||
|
# ]
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("", include("scan.urls")),
|
||||||
|
path("bad/", include("batches.urls")),
|
||||||
|
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")),
|
||||||
|
]
|
||||||
16
barcode/wsgi.py
Normal file
16
barcode/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for barcode project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'barcode.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
0
batches/__init__.py
Normal file
0
batches/__init__.py
Normal file
3
batches/admin.py
Normal file
3
batches/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
batches/apps.py
Normal file
6
batches/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BatchesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'batches'
|
||||||
0
batches/migrations/__init__.py
Normal file
0
batches/migrations/__init__.py
Normal file
3
batches/models.py
Normal file
3
batches/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
batches/tests.py
Normal file
3
batches/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
14
batches/urls.py
Normal file
14
batches/urls.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.urls import path
|
||||||
|
# from .views import ScanAPIView, ScanAspuAPIView, GetManagementAPIView
|
||||||
|
from .views import BatchListView, BatchListGTIN, BatchListReport, BatchReportView
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# path('api/scan/', ScanAPIView.as_view(), name='scan'),
|
||||||
|
# path('api/scan_aspu/', ScanAspuAPIView.as_view(), name='scan_aspu'),
|
||||||
|
path('batches/', BatchListView.as_view(), name='batch-list'),
|
||||||
|
path('gtin/', BatchListGTIN.as_view(), name='batch-gtin'),
|
||||||
|
path('reports/', BatchListReport.as_view(), name='batch-gtin'),
|
||||||
|
path('reports-view/', BatchReportView.as_view(), name='batch-gtin')
|
||||||
|
|
||||||
|
]
|
||||||
193
batches/views.py
Normal file
193
batches/views.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import requests
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.conf import settings
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
|
||||||
|
# http://192.168.254.2/szkm/api/batch/load/eb7bb39a-fd68-47c7-aec8-0041a4fe5faf/
|
||||||
|
|
||||||
|
# Настройки API
|
||||||
|
API_BASE_URL = "http://192.168.254.2:8280/api"
|
||||||
|
API_REPORTS_URL = f"{API_BASE_URL}/productlinereport/list"
|
||||||
|
LOGIN_URL = f"{API_BASE_URL}/login"
|
||||||
|
BATCH_LIST_URL = f"{API_BASE_URL}/Batch/list"
|
||||||
|
|
||||||
|
|
||||||
|
# Данные для входа
|
||||||
|
USERNAME = "user"
|
||||||
|
PASSWORD = "user"
|
||||||
|
|
||||||
|
def get_auth_token():
|
||||||
|
"""Авторизуется и получает Bearer токен"""
|
||||||
|
response = requests.post(LOGIN_URL, json={"username": USERNAME, "password": PASSWORD})
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
token = data.get("Value", {}).get("Token") # Исправленный способ получения токена
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BatchListView(View):
|
||||||
|
"""Получает список партий с фильтрацией по CreatedOn"""
|
||||||
|
def get(self, request):
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"Filter": {
|
||||||
|
"Filters": [
|
||||||
|
{"Value": "2024-01-1 00:00:00", "Operator": "gte", "Field": "CreatedOn"},
|
||||||
|
{"Value": "2025.11.22", "Operator": "lte", "Field": "CreatedOn"}
|
||||||
|
],
|
||||||
|
"Logic": "and"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(BATCH_LIST_URL, json=payload, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return JsonResponse(response.json(), safe=False)
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе данных"}, status=response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchListGTIN(View):
|
||||||
|
"""Получает список партий с фильтрацией по CreatedOn"""
|
||||||
|
def get(self, request):
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"Filter": {
|
||||||
|
"Filters": [
|
||||||
|
{
|
||||||
|
"Filters": [
|
||||||
|
{
|
||||||
|
"Value": "04607017161371",
|
||||||
|
"Operator": "eq",
|
||||||
|
"Field": "ProductType.GTIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Value": "04607017161371",
|
||||||
|
"Operator": "linq",
|
||||||
|
"Field": "CodesForProductLines.Any( a=> a.ProductType.GTIN=='{0}' )"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Logic": "or"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Logic": "and"
|
||||||
|
},
|
||||||
|
"Includes": [
|
||||||
|
"ProductType",
|
||||||
|
"CodesForProductLines.ProductType"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(BATCH_LIST_URL, json=payload, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return JsonResponse(response.json(), safe=False)
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе данных"}, status=response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class BatchListReport(View):
|
||||||
|
"""Получает список партий с фильтрацией по CreatedOn и BatchId из тела запроса"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = json.loads(request.body)
|
||||||
|
batch_id = body.get("id")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({"error": "Неверный формат JSON"}, status=400)
|
||||||
|
|
||||||
|
if not batch_id:
|
||||||
|
return JsonResponse({"error": "Отсутствует параметр 'id'"}, status=400)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
payload = {"skip":0,"take":10,"includes":["ProductLineBatch","ProductLineBatch.ProductType","ProductLine"],"sort":[{"field":"CreatedOn","dir":"desc"}],"filter":{"filters":[{"operator":"eq","field":"ProductLineBatchId","value":batch_id}],"logic":"and"}}
|
||||||
|
|
||||||
|
response = requests.post(API_REPORTS_URL, json=payload, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return JsonResponse(response.json(), safe=False)
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе данных"}, status=response.status_code)
|
||||||
|
class BatchReportView(View):
|
||||||
|
"""Получает список партий и прикрепляет к ним отчёты асинхронно."""
|
||||||
|
|
||||||
|
async def fetch_reports(self, session, headers, batch_id):
|
||||||
|
"""Асинхронно получает список отчётов для заданной партии."""
|
||||||
|
if not batch_id:
|
||||||
|
return []
|
||||||
|
|
||||||
|
report_payload = {
|
||||||
|
"Skip": 0,
|
||||||
|
"Take": 100000,
|
||||||
|
"Filter": {
|
||||||
|
"Field": "BatchId",
|
||||||
|
"Operator": "eq",
|
||||||
|
"Value": batch_id
|
||||||
|
},
|
||||||
|
"TotalCount": 0,
|
||||||
|
"Sort": [{"Field": "CreatedOn", "Dir": "desc"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.post(API_REPORTS_URL, json=report_payload, headers=headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
return data.get("Value", {}).get("Items", [])
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def fetch_all_reports(self, batches, headers):
|
||||||
|
"""Запрашивает отчёты для всех партий параллельно."""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
tasks = [self.fetch_reports(session, headers, batch.get("ProductTypeId")) for batch in batches]
|
||||||
|
reports = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# Присваиваем отчёты соответствующим партиям
|
||||||
|
for batch, report in zip(batches, reports):
|
||||||
|
batch["Reports"] = report
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""Основной метод обработки GET-запроса."""
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
# Запрос списка партий
|
||||||
|
batch_payload = {
|
||||||
|
"Filter": {
|
||||||
|
"Filters": [
|
||||||
|
{"Value": "2025-01-1 00:00:00", "Operator": "gte", "Field": "CreatedOn"},
|
||||||
|
{"Value": "2025.11.22", "Operator": "lte", "Field": "CreatedOn"}
|
||||||
|
],
|
||||||
|
"Logic": "and"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
batch_response = requests.post(BATCH_LIST_URL, json=batch_payload, headers=headers)
|
||||||
|
|
||||||
|
if batch_response.status_code != 200:
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе списка партий"}, status=batch_response.status_code)
|
||||||
|
|
||||||
|
batch_data = batch_response.json()
|
||||||
|
batches = batch_data.get("Value", {}).get("Items", [])
|
||||||
|
print(batches)
|
||||||
|
# Асинхронно получаем отчёты для всех партий
|
||||||
|
asyncio.run(self.fetch_all_reports(batches, headers))
|
||||||
|
print(fetch_all_reports)
|
||||||
|
return JsonResponse({"Batches": batches}, safe=False)
|
||||||
|
|
||||||
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
django:
|
||||||
|
build: .
|
||||||
|
container_name: django_app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- DEBUG=True
|
||||||
|
- DATABASE_URL=postgres://myuser:mypassword@db:5432/mydatabase
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: postgres_db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: myuser
|
||||||
|
POSTGRES_PASSWORD: mypassword
|
||||||
|
POSTGRES_DB: mydatabase
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
0
forms/__init__.py
Normal file
0
forms/__init__.py
Normal file
3
forms/admin.py
Normal file
3
forms/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
forms/apps.py
Normal file
6
forms/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FormsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'forms'
|
||||||
16
forms/forms.py
Normal file
16
forms/forms.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
class Step1Form(forms.Form):
|
||||||
|
линия = forms.CharField(label="Линия", max_length=100)
|
||||||
|
проблема = forms.CharField(label="Проблема", widget=forms.Textarea)
|
||||||
|
|
||||||
|
class Step2Form(forms.Form):
|
||||||
|
время_начала = forms.DateTimeField(label="Время начала проблемы", widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}))
|
||||||
|
время_решения = forms.DateTimeField(label="Время решения проблемы", widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}))
|
||||||
|
|
||||||
|
class Step3Form(forms.Form):
|
||||||
|
причина = forms.CharField(label="Причина простоя", widget=forms.Textarea)
|
||||||
|
устранение = forms.CharField(label="Способ устранения проблемы", widget=forms.Textarea)
|
||||||
30
forms/migrations/0001_initial.py
Normal file
30
forms/migrations/0001_initial.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# 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='FormResponse',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('factory', models.TextField()),
|
||||||
|
('line', models.CharField(max_length=100)),
|
||||||
|
('operators_name', models.TextField()),
|
||||||
|
('problem', models.TextField()),
|
||||||
|
('error_zone', models.TextField()),
|
||||||
|
('occurred_at', models.DateTimeField()),
|
||||||
|
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('downtime_reason', models.TextField()),
|
||||||
|
('fix_method', models.TextField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
forms/migrations/__init__.py
Normal file
0
forms/migrations/__init__.py
Normal file
17
forms/models.py
Normal file
17
forms/models.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class FormResponse(models.Model):
|
||||||
|
factory = models.TextField()
|
||||||
|
line = models.CharField(max_length=100) # <20><><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
operators_name = models.TextField() # <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
problem = models.TextField() # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
error_zone = models.TextField() # <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
occurred_at = models.DateTimeField() # <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
resolved_at = models.DateTimeField(null=True, blank=True) # <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
downtime_reason = models.TextField() # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
fix_method = models.TextField() # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True) # <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.line} - {self.problem} ({self.occurred_at})"
|
||||||
9
forms/serializers.py
Normal file
9
forms/serializers.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import FormResponse
|
||||||
|
|
||||||
|
class FormResponseSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = FormResponse
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
3
forms/tests.py
Normal file
3
forms/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
8
forms/urls.py
Normal file
8
forms/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import FormResponseCreateView, FormResponseListView, FormResponseUploadView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('submit/', FormResponseCreateView.as_view(), name='submit_response'), # POST-запрос
|
||||||
|
path('submit/uploads/', FormResponseUploadView.as_view(), name='form-upload-report'),
|
||||||
|
path('responses/', FormResponseListView.as_view(), name='list_responses'), # GET-запрос
|
||||||
|
]
|
||||||
90
forms/views.py
Normal file
90
forms/views.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
from .serializers import FormResponseSerializer
|
||||||
|
import pandas as pd
|
||||||
|
from rest_framework import generics, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from .models import FormResponse
|
||||||
|
import logging
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
import pandas as pd
|
||||||
|
from .models import FormResponse # Убедись, что модель импортирована
|
||||||
|
from .serializers import FormResponseSerializer # Импортируем сериализатор
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
# API для сохранения ответов
|
||||||
|
class FormResponseCreateView(generics.CreateAPIView):
|
||||||
|
queryset = FormResponse.objects.all()
|
||||||
|
serializer_class = FormResponseSerializer
|
||||||
|
|
||||||
|
# API для получения списка ответов
|
||||||
|
class FormResponseListView(generics.ListAPIView):
|
||||||
|
queryset = FormResponse.objects.all()
|
||||||
|
serializer_class = FormResponseSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# API для загрузки Excel-файла и сохранения в БД
|
||||||
|
logger = logging.getLogger(__name__) # Логирование ошибок
|
||||||
|
|
||||||
|
|
||||||
|
class FormResponseUploadView(APIView):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
print("\n===== [DEBUG] Данные, полученные на сервере =====")
|
||||||
|
print(json.dumps(request.data, indent=4, ensure_ascii=False)) # Логируем JSON
|
||||||
|
|
||||||
|
if "data" not in request.data:
|
||||||
|
return Response({"error": "Данные не найдены в запросе"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
uploaded_data = request.data["data"]
|
||||||
|
responses = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for item in uploaded_data:
|
||||||
|
# Обработка значений по умолчанию
|
||||||
|
operators_name = item.get("operators_name", "Анонимный пользователь") or "Анонимный пользователь"
|
||||||
|
error_zone = item.get("error_zone", "Не указано") or "Не указано"
|
||||||
|
downtime_reason = item.get("downtime_reason", "Не указано") or "Не указано"
|
||||||
|
fix_method = item.get("fix_method", "Не указано") or "Не указано"
|
||||||
|
|
||||||
|
# Функция для парсинга дат
|
||||||
|
def parse_date(date_str, default_now=False):
|
||||||
|
if not date_str:
|
||||||
|
return datetime.utcnow().isoformat() if default_now else None # Подставляем текущую дату если default_now=True
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(date_str.replace("Z", "+00:00")).isoformat()
|
||||||
|
except ValueError:
|
||||||
|
errors.append({"occurred_at": f"Неверный формат даты: {date_str}"})
|
||||||
|
return None
|
||||||
|
|
||||||
|
occurred_at = parse_date(item.get("occurred_at"), default_now=True) # Если нет даты — ставим текущее время
|
||||||
|
resolved_at = parse_date(item.get("resolved_at")) # Если нет, оставляем None
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
"line": item.get("line", "Неизвестная линия"),
|
||||||
|
"operators_name": operators_name,
|
||||||
|
"problem": item.get("problem", "Не указано"),
|
||||||
|
"error_zone": error_zone,
|
||||||
|
"occurred_at": occurred_at,
|
||||||
|
"resolved_at": resolved_at,
|
||||||
|
"downtime_reason": downtime_reason,
|
||||||
|
"fix_method": fix_method,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = FormResponseSerializer(data=response_data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
responses.append(serializer.save())
|
||||||
|
else:
|
||||||
|
errors.append(serializer.errors)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n===== [DEBUG] Ошибки при сохранении данных =====")
|
||||||
|
print(json.dumps(errors, indent=4, ensure_ascii=False)) # Логируем ошибки
|
||||||
|
return Response({"error": "Некоторые записи не были сохранены", "details": errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
return Response({"message": "Данные успешно загружены!", "saved": len(responses)}, status=status.HTTP_201_CREATED)
|
||||||
0
inventory/__init__.py
Normal file
0
inventory/__init__.py
Normal file
3
inventory/admin.py
Normal file
3
inventory/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
inventory/apps.py
Normal file
6
inventory/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'inventory'
|
||||||
69
inventory/migrations/0001_initial.py
Normal file
69
inventory/migrations/0001_initial.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
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)),
|
||||||
|
('gtin', models.CharField(max_length=14, unique=True)),
|
||||||
|
('unit', models.CharField(default='шт', max_length=50)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Sticker',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.IntegerField()),
|
||||||
|
('emission_date', models.DateField()),
|
||||||
|
('last_revision_date', models.DateField(blank=True, null=True)),
|
||||||
|
('location', models.CharField(max_length=255)),
|
||||||
|
('status', models.CharField(default='в наличии', max_length=255)),
|
||||||
|
('nomenclature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.nomenclature')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='StickerMovement',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('from_location', models.CharField(max_length=50)),
|
||||||
|
('to_location', models.CharField(max_length=50)),
|
||||||
|
('quantity', models.IntegerField()),
|
||||||
|
('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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
inventory/migrations/__init__.py
Normal file
0
inventory/migrations/__init__.py
Normal file
81
inventory/models.py
Normal file
81
inventory/models.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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)
|
||||||
|
gtin = models.CharField(max_length=14, unique=True)
|
||||||
|
unit = models.CharField(max_length=50, default="шт")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Sticker(models.Model):
|
||||||
|
nomenclature = models.ForeignKey("Nomenclature", on_delete=models.CASCADE)
|
||||||
|
quantity = models.IntegerField()
|
||||||
|
emission_date = models.DateField()
|
||||||
|
last_revision_date = models.DateField(null=True, blank=True)
|
||||||
|
location = models.CharField(max_length=255)
|
||||||
|
status = models.CharField(max_length=255, default="в наличии")
|
||||||
|
|
||||||
|
def is_expired(self):
|
||||||
|
"""Проверяем, истёк ли срок годности (1 год)"""
|
||||||
|
if not self.emission_date:
|
||||||
|
return False
|
||||||
|
expiration_date = self.emission_date + timedelta(days=365) # 12 месяцев
|
||||||
|
return date.today() > expiration_date
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""При сохранении автоматически обновляем статус"""
|
||||||
|
if self.is_expired():
|
||||||
|
self.status = "просрочены"
|
||||||
|
elif self.is_expiring_soon():
|
||||||
|
self.status = "скоро истекает"
|
||||||
|
else:
|
||||||
|
self.status = "в наличии"
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def is_expiring_soon(self):
|
||||||
|
"""Срок годности истекает через 1 месяц"""
|
||||||
|
if not self.emission_date:
|
||||||
|
return False
|
||||||
|
expiration_date = self.emission_date + timedelta(days=365)
|
||||||
|
return expiration_date - timedelta(days=30) <= date.today() < expiration_date
|
||||||
|
|
||||||
|
|
||||||
|
class StickerMovement(models.Model):
|
||||||
|
sticker = models.ForeignKey(Sticker, on_delete=models.CASCADE)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
from_location = models.CharField(max_length=50)
|
||||||
|
to_location = models.CharField(max_length=50)
|
||||||
|
quantity = models.IntegerField()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.from_location == "склад" and self.to_location == "цех":
|
||||||
|
self.sticker.location = "цех"
|
||||||
|
elif self.to_location == "склад":
|
||||||
|
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()
|
||||||
67
inventory/serializers.py
Normal file
67
inventory/serializers.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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):
|
||||||
|
class Meta:
|
||||||
|
model = Nomenclature
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class StickerSerializer(serializers.ModelSerializer):
|
||||||
|
is_expired = serializers.SerializerMethodField()
|
||||||
|
is_expiring_soon = serializers.SerializerMethodField()
|
||||||
|
nomenclature_name = serializers.CharField(source="nomenclature.name", read_only=True)
|
||||||
|
nomenclature_gtin = serializers.CharField(source="nomenclature.gtin", read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Sticker
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def get_is_expired(self, obj):
|
||||||
|
return obj.is_expired()
|
||||||
|
|
||||||
|
def get_is_expiring_soon(self, obj):
|
||||||
|
return obj.is_expiring_soon()
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
"""Обновляем статус при каждом запросе"""
|
||||||
|
instance.save()
|
||||||
|
return super().to_representation(instance)
|
||||||
|
|
||||||
|
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
|
||||||
21
inventory/serializers_transfer.py
Normal file
21
inventory/serializers_transfer.py
Normal 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
|
||||||
3
inventory/tests.py
Normal file
3
inventory/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
35
inventory/urls.py
Normal file
35
inventory/urls.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
127
inventory/views.py
Normal file
127
inventory/views.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, viewsets
|
||||||
|
from .models import Nomenclature, Sticker, StickerMovement
|
||||||
|
from .serializers import NomenclatureSerializer, StickerSerializer, StickerMovementSerializer
|
||||||
|
|
||||||
|
# Авторизационные данные
|
||||||
|
USERNAME = "superuser"
|
||||||
|
PASSWORD = "Superuser1105"
|
||||||
|
TOKEN_URL = "http://192.168.254.2:8280/api/login" # URL для получения токена
|
||||||
|
PRODUCT_LIST_URL = "http://192.168.254.2:8280/api/ProductType/list"
|
||||||
|
|
||||||
|
# Глобальный кэш для хранения токена
|
||||||
|
token_cache = {"token": None, "timestamp": 0}
|
||||||
|
TOKEN_LIFETIME = 1440 * 60 # 24 часа в секундах
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_token():
|
||||||
|
"""Получение нового токена с кешированием."""
|
||||||
|
global token_cache
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# Если токен есть и он еще не истек, возвращаем его
|
||||||
|
if token_cache["token"] and (current_time - token_cache["timestamp"] < TOKEN_LIFETIME):
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
payload = {"UserName": USERNAME, "Password": PASSWORD}
|
||||||
|
try:
|
||||||
|
response = requests.post(TOKEN_URL, json=payload, timeout=5)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and response_data.get("IsSuccess"):
|
||||||
|
token_cache["token"] = response_data["Value"]["Token"]
|
||||||
|
token_cache["timestamp"] = current_time
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
logging.error(f"Ошибка получения токена: {response_data}")
|
||||||
|
return None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка сети при получении токена: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalNomenclatureView(APIView):
|
||||||
|
"""Получение списка номенклатур с внешнего API."""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
product_line_id = request.data.get("product_line_id", "40ba525b-1686-4900-a2a9-443436812b1d")
|
||||||
|
token = get_new_token()
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return Response({"error": "Ошибка авторизации"}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"Filter": {
|
||||||
|
"Filters": [
|
||||||
|
{
|
||||||
|
"Logic": "and",
|
||||||
|
"Operator": "linq",
|
||||||
|
"Field": "ProductType_ProductLines.Where(i=>i.ProductLineId=='{0}').Any()",
|
||||||
|
"Value": product_line_id,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Logic": "and",
|
||||||
|
},
|
||||||
|
"Includes": ["ProductType_ProductLines"],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(PRODUCT_LIST_URL, json=payload, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return Response(response.json(), status=status.HTTP_200_OK)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка запроса {PRODUCT_LIST_URL}: {str(e)}")
|
||||||
|
return Response({"error": "Ошибка запроса к API"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
# Основные API ViewSet'ы
|
||||||
|
class NomenclatureViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Nomenclature.objects.all()
|
||||||
|
serializer_class = NomenclatureSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class StickerViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Sticker.objects.all().select_related('nomenclature')
|
||||||
|
serializer_class = StickerSerializer
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
data = request.data
|
||||||
|
gtin = data.get("nomenclature_gtin") # Берем GTIN из запроса
|
||||||
|
|
||||||
|
if not gtin:
|
||||||
|
return Response({"error": "GTIN обязателен"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Проверяем, есть ли номенклатура с таким GTIN
|
||||||
|
nomenclature, created = Nomenclature.objects.get_or_create(
|
||||||
|
gtin=gtin,
|
||||||
|
defaults={"name": data.get("nomenclature_name", "Неизвестный продукт")}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем данные запроса, чтобы стикер использовал ID найденной/созданной номенклатуры
|
||||||
|
data["nomenclature"] = nomenclature.id
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
return Response({"message": "Стикер успешно удалён"}, status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""Передача стикеров"""
|
||||||
|
|
||||||
|
class StickerMovementViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = StickerMovement.objects.all()
|
||||||
|
serializer_class = StickerMovementSerializer
|
||||||
|
|
||||||
58
inventory/views_transfer.py
Normal file
58
inventory/views_transfer.py
Normal 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
0
issues/__init__.py
Normal file
16
issues/admin.py
Normal file
16
issues/admin.py
Normal 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
6
issues/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IssuesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'issues'
|
||||||
59
issues/migrations/0001_initial.py
Normal file
59
issues/migrations/0001_initial.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
issues/migrations/__init__.py
Normal file
0
issues/migrations/__init__.py
Normal file
56
issues/models.py
Normal file
56
issues/models.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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]}"
|
||||||
33
issues/serializers.py
Normal file
33
issues/serializers.py
Normal 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
3
issues/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
16
issues/urls.py
Normal file
16
issues/urls.py
Normal 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
29
issues/views.py
Normal 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
|
||||||
22
manage.py
Normal file
22
manage.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'barcode.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
321
mobileApps/fletFormTest.py
Normal file
321
mobileApps/fletFormTest.py
Normal 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л", "5л"],
|
||||||
|
"Скит": ["Sipa", "Devin", "JR", "19 Литров", "5 Литров"]
|
||||||
|
}
|
||||||
|
|
||||||
|
line_zone_map = {
|
||||||
|
"Sipa": ["Сериализация", "Упаковка", "Сборка палеты", "Закрытие палеты"],
|
||||||
|
"Devin": ["Сериализация", "Упаковка", "Сборка палеты"],
|
||||||
|
"JR": ["Сериализация", "Упаковка", "Сборка палеты", "Закрытие палеты"],
|
||||||
|
"19 Литров": ["Валидация", "Тонкий клиент"],
|
||||||
|
"5 Литров": ["Валидация", "Тонкий клиент"],
|
||||||
|
"ПЭТ": ["Сериализация", "Упаковка", "Сборка палеты", "Закрытие палеты"],
|
||||||
|
"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
0
production/__init__.py
Normal file
3
production/admin.py
Normal file
3
production/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
production/apps.py
Normal file
6
production/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'production'
|
||||||
27
production/migrations/0001_initial.py
Normal file
27
production/migrations/0001_initial.py
Normal 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='Создано')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
production/migrations/__init__.py
Normal file
0
production/migrations/__init__.py
Normal file
13
production/models.py
Normal file
13
production/models.py
Normal 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}"
|
||||||
7
production/serializers.py
Normal file
7
production/serializers.py
Normal 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
3
production/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
10
production/urls.py
Normal file
10
production/urls.py
Normal 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
7
production/views.py
Normal 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
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
0
scan/__init__.py
Normal file
0
scan/__init__.py
Normal file
3
scan/admin.py
Normal file
3
scan/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
53
scan/api_client.py
Normal file
53
scan/api_client.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# api_client.py
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from .constants import API_URLS, CREDENTIALS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class APIClient:
|
||||||
|
@staticmethod
|
||||||
|
def get_token(url):
|
||||||
|
"""Получение токена авторизации"""
|
||||||
|
payload = {
|
||||||
|
"UserName": CREDENTIALS["USERNAME"],
|
||||||
|
"Password": CREDENTIALS["PASSWORD"]
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
return data["Value"]["Token"] if data.get("IsSuccess") else None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Token request error: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_request(url, token, payload=None, timeout=10):
|
||||||
|
"""Выполнение API-запроса"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
json=payload or {},
|
||||||
|
headers=headers,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"API request error: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_response(status, message=None, data=None, status_code=200):
|
||||||
|
"""Формирование стандартного ответа API"""
|
||||||
|
return Response({
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
"data": data
|
||||||
|
}, status=status_code)
|
||||||
6
scan/apps.py
Normal file
6
scan/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ScanConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'scan'
|
||||||
11
scan/celery_app.py
Normal file
11
scan/celery_app.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
# Указываем путь к настройкам Django
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "barcode.settings")
|
||||||
|
|
||||||
|
app = Celery("barcode")
|
||||||
|
|
||||||
|
# Загружаем настройки Celery из Django
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
19
scan/constants.py
Normal file
19
scan/constants.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# constants.py
|
||||||
|
API_URLS = {
|
||||||
|
"LOGIN": "http://192.168.254.2:8280/api/login",
|
||||||
|
"LOGIN_ASPU": "http://192.168.254.10:3428/api/login",
|
||||||
|
"STORAGE_INFO": "http://192.168.254.2:8180/api/Storage/getInfo",
|
||||||
|
"ASPU_CODE_INFO": "http://192.168.254.10:3428/api/Codes/getInfo",
|
||||||
|
"MANAGEMENT_ENDPOINTS": {
|
||||||
|
"Sipa": "http://192.168.254.10:3428/api/Management/",
|
||||||
|
"Devin": "http://192.168.254.20:3428/api/Management/",
|
||||||
|
"JR": "http://192.168.254.30:3428/api/Management/",
|
||||||
|
"5L": "http://192.168.254.40:3428/api/Management/",
|
||||||
|
"19L": "http://192.168.254.50:3428/api/Management/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CREDENTIALS = {
|
||||||
|
"USERNAME": "user",
|
||||||
|
"PASSWORD": "user"
|
||||||
|
}
|
||||||
32
scan/migrations/0001_initial.py
Normal file
32
scan/migrations/0001_initial.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# 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='MessagesAlert',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type_message', models.CharField(max_length=255, unique=True)),
|
||||||
|
('product_line', models.CharField(max_length=255, unique=True)),
|
||||||
|
('code_lexer', models.IntegerField()),
|
||||||
|
('time', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ScannedCode',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=255, unique=True)),
|
||||||
|
('scanned_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
scan/migrations/__init__.py
Normal file
0
scan/migrations/__init__.py
Normal file
15
scan/models.py
Normal file
15
scan/models.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class ScannedCode(models.Model):
|
||||||
|
code = models.CharField(max_length=255, unique=True)
|
||||||
|
scanned_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
class MessagesAlert(models.Model):
|
||||||
|
type_message = models.CharField(max_length=255, unique=True)
|
||||||
|
product_line = models.CharField(max_length=255, unique=True)
|
||||||
|
code_lexer = models.IntegerField()
|
||||||
|
time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
7
scan/serializers.py
Normal file
7
scan/serializers.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import ScannedCode
|
||||||
|
|
||||||
|
class ScannedCodeSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ScannedCode
|
||||||
|
fields = "__all__"
|
||||||
13
scan/tasks.py
Normal file
13
scan/tasks.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
import requests
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def fetch_api_data(url, token):
|
||||||
|
"""Запрос к API в фоновом режиме"""
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, timeout=10)
|
||||||
|
if response.ok:
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException:
|
||||||
|
return None
|
||||||
3
scan/tests.py
Normal file
3
scan/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
10
scan/urls.py
Normal file
10
scan/urls.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
# from .views import ScanAPIView, ScanAspuAPIView, GetManagementAPIView
|
||||||
|
from .views import GetManagementAPIView
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# path('api/scan/', ScanAPIView.as_view(), name='scan'),
|
||||||
|
# path('api/scan_aspu/', ScanAspuAPIView.as_view(), name='scan_aspu'),
|
||||||
|
path('api/get_management/', GetManagementAPIView.as_view(), name='get_management'),
|
||||||
|
]
|
||||||
83
scan/utils.py
Normal file
83
scan/utils.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from .constants import *
|
||||||
|
|
||||||
|
def get_new_token(url):
|
||||||
|
|
||||||
|
"""Function to get a new token."""
|
||||||
|
|
||||||
|
payload = {"UserName": USERNAME, "Password": PASSWORD}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload)
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and response_data.get("IsSuccess"):
|
||||||
|
|
||||||
|
return response_data["Value"]["Token"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
|
||||||
|
logging.error(f"Network error while getting token: {str(e)}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def request_to_api(url, token, payload=None, timeout=10):
|
||||||
|
|
||||||
|
"""General function to make API requests."""
|
||||||
|
|
||||||
|
if payload is None:
|
||||||
|
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload, headers=headers, timeout=timeout)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
logging.error(f"Error from API ({url}): {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except requests.Timeout:
|
||||||
|
|
||||||
|
logging.error(f"Timeout while requesting {url}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
|
||||||
|
logging.error(f"Request error to {url}: {str(e)}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_response(status, message=None, data=None, status_code=200):
|
||||||
|
|
||||||
|
"""Function to create consistent API responses."""
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
|
||||||
|
"status": status,
|
||||||
|
|
||||||
|
"message": message,
|
||||||
|
|
||||||
|
"data": data
|
||||||
|
|
||||||
|
}, status=status_code)
|
||||||
215
scan/views.py
Normal file
215
scan/views.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from .models import ScannedCode
|
||||||
|
from .serializers import ScannedCodeSerializer
|
||||||
|
|
||||||
|
# Константы API URL и креденшелы
|
||||||
|
LOGIN_URL_ASPU = "http://192.168.254.10:3428/api/login"
|
||||||
|
API_URL_ASPU_MANAGEMENT = "http://192.168.254.10:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_DEVIN = "http://192.168.254.20:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_JR = "http://192.168.254.30:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_5L = "http://192.168.254.40:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_19L = "http://192.168.254.50:3428/api/Management/"
|
||||||
|
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_SIPA = "http://192.168.254.10:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_DEVIN = "http://192.168.254.20:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_JR = "http://192.168.254.30:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_5L = "http://192.168.254.40:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_19L = "http://192.168.254.50:3428/api/Messages/"
|
||||||
|
|
||||||
|
USERNAME = "superuser"
|
||||||
|
PASSWORD = "Superuser1105"
|
||||||
|
|
||||||
|
# Глобальные переменные для хранения токена
|
||||||
|
token_cache = {"token": None, "timestamp": 0}
|
||||||
|
TOKEN_LIFETIME = 1440 * 60 # 24 часа в секундах
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_token(url):
|
||||||
|
"""Получение нового токена с кешированием."""
|
||||||
|
global token_cache
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if token_cache["token"] and (current_time - token_cache["timestamp"] < TOKEN_LIFETIME):
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
payload = {"UserName": USERNAME, "Password": PASSWORD}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, timeout=5)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and response_data.get("IsSuccess"):
|
||||||
|
token_cache["token"] = response_data["Value"]["Token"]
|
||||||
|
token_cache["timestamp"] = current_time
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
logging.error(f"Ошибка получения токена: {response_data}")
|
||||||
|
return None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка сети при получении токена: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def request_to_api(url, token, payload=None, timeout=10):
|
||||||
|
"""Отправка запроса к API."""
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload or {}, headers=headers, timeout=timeout)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.Timeout:
|
||||||
|
logging.error(f"Тайм-аут при запросе {url}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка запроса {url}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_product_id(text):
|
||||||
|
"""Функция извлечения идентификатора продукта из текста."""
|
||||||
|
import re
|
||||||
|
match = re.search(r'Product\s*ID:\s*(\d+)', text)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def create_response(status, message=None, data=None, status_code=200):
|
||||||
|
"""Создание стандартного ответа API."""
|
||||||
|
return Response({
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
"data": data
|
||||||
|
}, status=status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class GetManagementAPIView(APIView):
|
||||||
|
"""Получение данных управления и сообщений с различных источников."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
token = get_new_token(LOGIN_URL_ASPU)
|
||||||
|
if not token:
|
||||||
|
return create_response("Error", message="Failed to get token", status_code=500)
|
||||||
|
|
||||||
|
management_urls = {
|
||||||
|
"Sipa": API_URL_ASPU_MANAGEMENT,
|
||||||
|
"Devin": API_URL_MANAGEMENT_DEVIN,
|
||||||
|
"JR": API_URL_MANAGEMENT_JR,
|
||||||
|
"5L": API_URL_MANAGEMENT_5L,
|
||||||
|
"19L": API_URL_MANAGEMENT_19L
|
||||||
|
}
|
||||||
|
|
||||||
|
message_urls = {
|
||||||
|
"Sipa": API_URL_ASPU_MASSAGE_LIST_SIPA,
|
||||||
|
"Devin": API_URL_ASPU_MASSAGE_LIST_DEVIN,
|
||||||
|
"JR": API_URL_ASPU_MASSAGE_LIST_JR,
|
||||||
|
"5L": API_URL_ASPU_MASSAGE_LIST_5L,
|
||||||
|
"19L": API_URL_ASPU_MASSAGE_LIST_19L
|
||||||
|
}
|
||||||
|
|
||||||
|
message_payload = {
|
||||||
|
"skip": 1,
|
||||||
|
"take": 50,
|
||||||
|
"includes": [],
|
||||||
|
"sort": [{"field": "Time", "dir": "desc"}],
|
||||||
|
"filter": {"filters": [], "logic": "and"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Используем defaultdict, чтобы избежать KeyError
|
||||||
|
all_data = defaultdict(lambda: {
|
||||||
|
"Name": "",
|
||||||
|
"ShortName": "",
|
||||||
|
"BatchName": "",
|
||||||
|
"Stats": [],
|
||||||
|
"Sources": [],
|
||||||
|
"Gtin": "",
|
||||||
|
"GroupType": "",
|
||||||
|
"Quantity": 0,
|
||||||
|
"Messages": []
|
||||||
|
})
|
||||||
|
|
||||||
|
def process_management_response(key, response_data):
|
||||||
|
"""Обработка данных управления."""
|
||||||
|
if not response_data or "Value" not in response_data:
|
||||||
|
logging.error(f"Некорректный ответ от {key}: {response_data}")
|
||||||
|
return
|
||||||
|
|
||||||
|
value_data = response_data["Value"]
|
||||||
|
batch_name = value_data.get("BatchName", "")
|
||||||
|
|
||||||
|
for product in value_data.get("ProductTypes", []):
|
||||||
|
product_id = product.get("Id")
|
||||||
|
if not product_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
all_data[product_id].update({
|
||||||
|
"Name": product.get("Name", ""),
|
||||||
|
"ShortName": product.get("ShortName", ""),
|
||||||
|
"BatchName": batch_name,
|
||||||
|
"Gtin": product.get("Gtin", ""),
|
||||||
|
"GroupType": product.get("GroupType", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
all_data[product_id]["Stats"].extend(product.get("Stats", []))
|
||||||
|
all_data[product_id]["Sources"].append(key)
|
||||||
|
|
||||||
|
if product.get("GroupType") == "Pallet":
|
||||||
|
all_data[product_id]["Quantity"] = sum(
|
||||||
|
stat["Value"] for stat in product.get("Stats", []) if stat.get("Type") == "Validated"
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_message_response(key, response_data):
|
||||||
|
"""Обработка сообщений."""
|
||||||
|
if not response_data or "Value" not in response_data:
|
||||||
|
logging.error(f"Некорректный ответ от {key}: {response_data}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for msg in response_data["Value"]:
|
||||||
|
if "Text" not in msg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"Id": msg["Id"],
|
||||||
|
"Time": msg["Time"],
|
||||||
|
"Type": msg["Type"],
|
||||||
|
"Text": msg["Text"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем в общий список сообщений
|
||||||
|
for product_id in all_data:
|
||||||
|
all_data[product_id]["Messages"].append(message)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
|
futures = {
|
||||||
|
executor.submit(request_to_api, url, token, timeout=2): (key, "management")
|
||||||
|
for key, url in management_urls.items()
|
||||||
|
}
|
||||||
|
futures.update({
|
||||||
|
executor.submit(request_to_api, url, token, payload=message_payload, timeout=2): (key, "messages")
|
||||||
|
for key, url in message_urls.items()
|
||||||
|
})
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
key, request_type = futures[future]
|
||||||
|
try:
|
||||||
|
response_data = future.result()
|
||||||
|
|
||||||
|
if request_type == "management":
|
||||||
|
process_management_response(key, response_data)
|
||||||
|
elif request_type == "messages":
|
||||||
|
process_message_response(key, response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Ошибка обработки {key}: {str(e)}")
|
||||||
|
|
||||||
|
logging.info(f"Отправляем ответ с {len(all_data)} объектами управления")
|
||||||
|
|
||||||
|
return create_response("OK", data=dict(all_data)) if all_data else create_response(
|
||||||
|
"Error", message="No data found", status_code=404
|
||||||
|
)
|
||||||
3
scan/viewss1/__init__.py
Normal file
3
scan/viewss1/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# views/__init__.py
|
||||||
|
from .scan_views import StorageScanView, AspuScanView
|
||||||
|
from .management_views import ManagementDataView
|
||||||
88
scan/viewss1/management_views.py
Normal file
88
scan/viewss1/management_views.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# views/management_views.py
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
import logging
|
||||||
|
from ..api_client import APIClient
|
||||||
|
from ..constants import API_URLS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ManagementDataView(APIView):
|
||||||
|
"""Получение управленческих данных"""
|
||||||
|
client = APIClient()
|
||||||
|
response_handler = APIClient.create_response
|
||||||
|
MAX_WORKERS = 5
|
||||||
|
REQUEST_TIMEOUT = 5
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
token = self.client.get_token(API_URLS["LOGIN_ASPU"])
|
||||||
|
if not token:
|
||||||
|
return self.response_handler(
|
||||||
|
"Error",
|
||||||
|
"Failed to get token",
|
||||||
|
status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
all_data = {}
|
||||||
|
with ThreadPoolExecutor(max_workers=self.MAX_WORKERS) as executor:
|
||||||
|
futures = {
|
||||||
|
executor.submit(
|
||||||
|
self.client.make_request,
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
timeout=self.REQUEST_TIMEOUT
|
||||||
|
): name
|
||||||
|
for name, url in API_URLS["MANAGEMENT_ENDPOINTS"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
name = futures[future]
|
||||||
|
try:
|
||||||
|
self.process_response(
|
||||||
|
name,
|
||||||
|
future.result(),
|
||||||
|
all_data
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing {name}: {str(e)}")
|
||||||
|
|
||||||
|
return self.response_handler(
|
||||||
|
"OK" if all_data else "Error",
|
||||||
|
data=all_data or None,
|
||||||
|
status_code=200 if all_data else 404
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_response(self, source_name, response, data_store):
|
||||||
|
"""Обработка полученных данных"""
|
||||||
|
if not response or not response.get("Value"):
|
||||||
|
return
|
||||||
|
|
||||||
|
value_data = response["Value"]
|
||||||
|
batch_name = value_data.get("BatchName")
|
||||||
|
|
||||||
|
for product in value_data.get("ProductTypes", []):
|
||||||
|
product_id = product.get("Id")
|
||||||
|
if not product_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if product_id not in data_store:
|
||||||
|
data_store[product_id] = {
|
||||||
|
"Name": product.get("Name"),
|
||||||
|
"ShortName": product.get("ShortName"),
|
||||||
|
"BatchName": batch_name,
|
||||||
|
"Stats": [],
|
||||||
|
"Sources": [],
|
||||||
|
"Gtin": product.get("Gtin"),
|
||||||
|
"GroupType": product.get("GroupType", ""),
|
||||||
|
"Quantity": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
data_store[product_id]["Stats"].extend(product.get("Stats", []))
|
||||||
|
data_store[product_id]["Sources"].append(source_name)
|
||||||
|
|
||||||
|
if product.get("GroupType") == "Pallet":
|
||||||
|
data_store[product_id]["Quantity"] = sum(
|
||||||
|
stat["Value"]
|
||||||
|
for stat in product.get("Stats", [])
|
||||||
|
if stat["Type"] == "Validated"
|
||||||
|
)
|
||||||
77
scan/viewss1/scan_views.py
Normal file
77
scan/viewss1/scan_views.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# views/scan_views.py
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from ..api_client import APIClient
|
||||||
|
from ..constants import API_URLS
|
||||||
|
|
||||||
|
|
||||||
|
class BaseScanView(APIView):
|
||||||
|
"""Базовый класс для сканирования"""
|
||||||
|
client = APIClient()
|
||||||
|
response_handler = APIClient.create_response
|
||||||
|
|
||||||
|
def validate_code(self, code):
|
||||||
|
if not code:
|
||||||
|
return self.response_handler(
|
||||||
|
"Error",
|
||||||
|
"Code not found",
|
||||||
|
status_code=400
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class StorageScanView(BaseScanView):
|
||||||
|
"""Сканирование кодов хранилища"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
raw_code = request.data.get("code")
|
||||||
|
if error := self.validate_code(raw_code):
|
||||||
|
return error
|
||||||
|
|
||||||
|
formatted_code = raw_code.replace("\u001d", chr(29))
|
||||||
|
token = self.client.get_token(API_URLS["LOGIN"])
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return self.response_handler(
|
||||||
|
"Error",
|
||||||
|
"Failed to get token",
|
||||||
|
status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
response_data = self.client.make_request(
|
||||||
|
API_URLS["STORAGE_INFO"],
|
||||||
|
token,
|
||||||
|
{"IdentificationCode": formatted_code}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response_data and response_data.get("Value"):
|
||||||
|
return self.response_handler("OK", data=response_data)
|
||||||
|
return self.response_handler("Error", "Data not found", status_code=404)
|
||||||
|
|
||||||
|
|
||||||
|
class AspuScanView(BaseScanView):
|
||||||
|
"""Сканирование ASPU кодов"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
raw_code = request.data.get("code")
|
||||||
|
if error := self.validate_code(raw_code):
|
||||||
|
return error
|
||||||
|
|
||||||
|
formatted_code = raw_code.replace("\x1d", "\\u001d")
|
||||||
|
token = self.client.get_token(API_URLS["LOGIN_ASPU"])
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return self.response_handler(
|
||||||
|
"Error",
|
||||||
|
"Failed to get token",
|
||||||
|
status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
response_data = self.client.make_request(
|
||||||
|
API_URLS["ASPU_CODE_INFO"],
|
||||||
|
token,
|
||||||
|
{"code": formatted_code}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response_data and response_data.get("Value"):
|
||||||
|
return self.response_handler("OK", data=response_data)
|
||||||
|
return self.response_handler("Error", "Data not found", status_code=404)
|
||||||
0
users/__init__.py
Normal file
0
users/__init__.py
Normal file
3
users/admin.py
Normal file
3
users/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
users/apps.py
Normal file
6
users/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'users'
|
||||||
0
users/migrations/__init__.py
Normal file
0
users/migrations/__init__.py
Normal file
3
users/models.py
Normal file
3
users/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
28
users/serializers.py
Normal file
28
users/serializers.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
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
|
||||||
|
|
||||||
|
user = authenticate(username=data["username"], password=data["password"])
|
||||||
|
if not user:
|
||||||
|
raise serializers.ValidationError("Неверный логин или пароль")
|
||||||
|
|
||||||
|
refresh = RefreshToken.for_user(user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"username": user.username,
|
||||||
|
"profile": self.get_profile_name(user), # Возвращаем профильное имя
|
||||||
|
"access": str(refresh.access_token),
|
||||||
|
"refresh": str(refresh)
|
||||||
|
}
|
||||||
3
users/tests.py
Normal file
3
users/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
6
users/urls.py
Normal file
6
users/urls.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import UserLoginView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("login/", UserLoginView.as_view(), name="user-login"),
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user