From adc0c142788e00e994825375f40e81370a0c1023 Mon Sep 17 00:00:00 2001 From: The unwasted guests Date: Sun, 23 Mar 2025 21:24:08 +1000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 3 + .idea/backend-fontend.iml | 14 + .idea/encodings.xml | 6 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + Dockerfile | 18 + __init__.py | 1 + barcode/__init__.py | 1 + barcode/asgi.py | 20 ++ barcode/celery_app.py | 11 + barcode/consumers.py | 20 ++ barcode/data_fetcher.py | 63 ++++ barcode/routing.py | 6 + barcode/settings.py | 217 ++++++++++++ barcode/urls.py | 35 ++ barcode/wsgi.py | 16 + batches/__init__.py | 0 batches/admin.py | 3 + batches/apps.py | 6 + batches/migrations/__init__.py | 0 batches/models.py | 3 + batches/tests.py | 3 + batches/urls.py | 14 + batches/views.py | 193 +++++++++++ db.sqlite3 | Bin 0 -> 139264 bytes docker-compose.yml | 32 ++ forms/__init__.py | 0 forms/admin.py | 3 + forms/apps.py | 6 + forms/forms.py | 16 + forms/migrations/0001_initial.py | 30 ++ forms/migrations/__init__.py | 0 forms/models.py | 17 + forms/serializers.py | 9 + forms/tests.py | 3 + forms/urls.py | 8 + forms/views.py | 90 +++++ inventory/__init__.py | 0 inventory/admin.py | 3 + inventory/apps.py | 6 + inventory/migrations/0001_initial.py | 69 ++++ inventory/migrations/__init__.py | 0 inventory/models.py | 81 +++++ inventory/serializers.py | 67 ++++ inventory/serializers_transfer.py | 21 ++ inventory/tests.py | 3 + inventory/urls.py | 35 ++ inventory/views.py | 127 +++++++ inventory/views_transfer.py | 58 ++++ issues/__init__.py | 0 issues/admin.py | 16 + issues/apps.py | 6 + issues/migrations/0001_initial.py | 59 ++++ issues/migrations/__init__.py | 0 issues/models.py | 56 +++ issues/serializers.py | 33 ++ issues/tests.py | 3 + issues/urls.py | 16 + issues/views.py | 29 ++ manage.py | 22 ++ mobileApps/fletFormTest.py | 321 ++++++++++++++++++ production/__init__.py | 0 production/admin.py | 3 + production/apps.py | 6 + production/migrations/0001_initial.py | 27 ++ production/migrations/__init__.py | 0 production/models.py | 13 + production/serializers.py | 7 + production/tests.py | 3 + production/urls.py | 10 + production/views.py | 7 + requirements.txt | Bin 0 -> 6426 bytes scan/__init__.py | 0 scan/admin.py | 3 + scan/api_client.py | 53 +++ scan/apps.py | 6 + scan/celery_app.py | 11 + scan/constants.py | 19 ++ scan/migrations/0001_initial.py | 32 ++ scan/migrations/__init__.py | 0 scan/models.py | 15 + scan/serializers.py | 7 + scan/tasks.py | 13 + scan/tests.py | 3 + scan/urls.py | 10 + scan/utils.py | 83 +++++ scan/views.py | 215 ++++++++++++ scan/viewss1/__init__.py | 3 + scan/viewss1/management_views.py | 88 +++++ scan/viewss1/scan_views.py | 77 +++++ users/__init__.py | 0 users/admin.py | 3 + users/apps.py | 6 + users/migrations/__init__.py | 0 users/models.py | 3 + users/serializers.py | 28 ++ users/tests.py | 3 + users/urls.py | 6 + users/views.py | 11 + 101 files changed, 2699 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/backend-fontend.iml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Dockerfile create mode 100644 __init__.py create mode 100644 barcode/__init__.py create mode 100644 barcode/asgi.py create mode 100644 barcode/celery_app.py create mode 100644 barcode/consumers.py create mode 100644 barcode/data_fetcher.py create mode 100644 barcode/routing.py create mode 100644 barcode/settings.py create mode 100644 barcode/urls.py create mode 100644 barcode/wsgi.py create mode 100644 batches/__init__.py create mode 100644 batches/admin.py create mode 100644 batches/apps.py create mode 100644 batches/migrations/__init__.py create mode 100644 batches/models.py create mode 100644 batches/tests.py create mode 100644 batches/urls.py create mode 100644 batches/views.py create mode 100644 db.sqlite3 create mode 100644 docker-compose.yml create mode 100644 forms/__init__.py create mode 100644 forms/admin.py create mode 100644 forms/apps.py create mode 100644 forms/forms.py create mode 100644 forms/migrations/0001_initial.py create mode 100644 forms/migrations/__init__.py create mode 100644 forms/models.py create mode 100644 forms/serializers.py create mode 100644 forms/tests.py create mode 100644 forms/urls.py create mode 100644 forms/views.py create mode 100644 inventory/__init__.py create mode 100644 inventory/admin.py create mode 100644 inventory/apps.py create mode 100644 inventory/migrations/0001_initial.py create mode 100644 inventory/migrations/__init__.py create mode 100644 inventory/models.py create mode 100644 inventory/serializers.py create mode 100644 inventory/serializers_transfer.py create mode 100644 inventory/tests.py create mode 100644 inventory/urls.py create mode 100644 inventory/views.py create mode 100644 inventory/views_transfer.py create mode 100644 issues/__init__.py create mode 100644 issues/admin.py create mode 100644 issues/apps.py create mode 100644 issues/migrations/0001_initial.py create mode 100644 issues/migrations/__init__.py create mode 100644 issues/models.py create mode 100644 issues/serializers.py create mode 100644 issues/tests.py create mode 100644 issues/urls.py create mode 100644 issues/views.py create mode 100644 manage.py create mode 100644 mobileApps/fletFormTest.py create mode 100644 production/__init__.py create mode 100644 production/admin.py create mode 100644 production/apps.py create mode 100644 production/migrations/0001_initial.py create mode 100644 production/migrations/__init__.py create mode 100644 production/models.py create mode 100644 production/serializers.py create mode 100644 production/tests.py create mode 100644 production/urls.py create mode 100644 production/views.py create mode 100644 requirements.txt create mode 100644 scan/__init__.py create mode 100644 scan/admin.py create mode 100644 scan/api_client.py create mode 100644 scan/apps.py create mode 100644 scan/celery_app.py create mode 100644 scan/constants.py create mode 100644 scan/migrations/0001_initial.py create mode 100644 scan/migrations/__init__.py create mode 100644 scan/models.py create mode 100644 scan/serializers.py create mode 100644 scan/tasks.py create mode 100644 scan/tests.py create mode 100644 scan/urls.py create mode 100644 scan/utils.py create mode 100644 scan/views.py create mode 100644 scan/viewss1/__init__.py create mode 100644 scan/viewss1/management_views.py create mode 100644 scan/viewss1/scan_views.py create mode 100644 users/__init__.py create mode 100644 users/admin.py create mode 100644 users/apps.py create mode 100644 users/migrations/__init__.py create mode 100644 users/models.py create mode 100644 users/serializers.py create mode 100644 users/tests.py create mode 100644 users/urls.py create mode 100644 users/views.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/backend-fontend.iml b/.idea/backend-fontend.iml new file mode 100644 index 0000000..7a6134d --- /dev/null +++ b/.idea/backend-fontend.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..ed69453 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..03b87a6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1155bf3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5554078 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..4b09802 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .celery_app import app as celery_app diff --git a/barcode/__init__.py b/barcode/__init__.py new file mode 100644 index 0000000..4b09802 --- /dev/null +++ b/barcode/__init__.py @@ -0,0 +1 @@ +from .celery_app import app as celery_app diff --git a/barcode/asgi.py b/barcode/asgi.py new file mode 100644 index 0000000..df29ea8 --- /dev/null +++ b/barcode/asgi.py @@ -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), +}) diff --git a/barcode/celery_app.py b/barcode/celery_app.py new file mode 100644 index 0000000..c2a8f87 --- /dev/null +++ b/barcode/celery_app.py @@ -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() diff --git a/barcode/consumers.py b/barcode/consumers.py new file mode 100644 index 0000000..5d756be --- /dev/null +++ b/barcode/consumers.py @@ -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 секунд перед следующим запросом diff --git a/barcode/data_fetcher.py b/barcode/data_fetcher.py new file mode 100644 index 0000000..38f8070 --- /dev/null +++ b/barcode/data_fetcher.py @@ -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 diff --git a/barcode/routing.py b/barcode/routing.py new file mode 100644 index 0000000..cc78f28 --- /dev/null +++ b/barcode/routing.py @@ -0,0 +1,6 @@ +from django.urls import re_path +from .consumers import LineDataConsumer + +websocket_urlpatterns = [ + re_path(r"ws/lines/$", LineDataConsumer.as_asgi()), +] diff --git a/barcode/settings.py b/barcode/settings.py new file mode 100644 index 0000000..06bdbfe --- /dev/null +++ b/barcode/settings.py @@ -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 + +# ������������� UTF-8 �� ��������� +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', # ��������� ������ JSON + ), +} + +CORS_ALLOW_HEADERS = [ + 'authorization', + 'content-type', + 'x-csrftoken', + 'x-requested-with', +] + +CORS_ALLOW_ALL_ORIGINS = True # ��������� ������� �� ���� ���������� +# ��� ��������� ������ 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, # Увеличь лимит соединений + }, + }, +} + diff --git a/barcode/urls.py b/barcode/urls.py new file mode 100644 index 0000000..b94948d --- /dev/null +++ b/barcode/urls.py @@ -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")), +] \ No newline at end of file diff --git a/barcode/wsgi.py b/barcode/wsgi.py new file mode 100644 index 0000000..c250e38 --- /dev/null +++ b/barcode/wsgi.py @@ -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() diff --git a/batches/__init__.py b/batches/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/batches/admin.py b/batches/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/batches/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/batches/apps.py b/batches/apps.py new file mode 100644 index 0000000..27433ae --- /dev/null +++ b/batches/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BatchesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'batches' diff --git a/batches/migrations/__init__.py b/batches/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/batches/models.py b/batches/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/batches/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/batches/tests.py b/batches/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/batches/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/batches/urls.py b/batches/urls.py new file mode 100644 index 0000000..feae665 --- /dev/null +++ b/batches/urls.py @@ -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') + +] \ No newline at end of file diff --git a/batches/views.py b/batches/views.py new file mode 100644 index 0000000..dc6aa83 --- /dev/null +++ b/batches/views.py @@ -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) + diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..25c14b2b26e453487d5cf226805407d28ad3e0be GIT binary patch literal 139264 zcmeI5U2GfKb;mg(#Stx$BTJTN*OsFZwep%PYsGJhyPJ5mHrH0xme;b}SPzTAkQ~YM zQlv~${=lMXD7zap{Xm^8YhBR)g6+K5>9pth+rKo(gr%P_8Cm*C@Wk*BhffawS>OZl2Z68p z-yPia{f75@13w+OEPT=PtzL=_dcS#QOqAc87n=1_rKsQ4b{aR-ow{CAH*3|M?YePZ zDjJt6on(`VRJ0H+s5_O?EvNYS+11?AT28sPvV7%QPFY?#m%FZnol^TK4J(&dx-*59 zQ(WSPdL(i;jk!gW`(9^d0?b&>$R;?yZ*0loLEd2QiW_>=azK| zkC|TB&F>QGq0wf1R~F_%haF`6+^8tOLB?O&%XmW**`1HYqlNrN*4^&I2eP|M*?uOs zGLW@5r6IZL%?i6S2Qs(ASv6WHCgQ1l)M`0hghqR}pDtOeN$ye1fqkPU)uD!*eI`Uz zGuKfyHjzTL($FgnwQ+A-XH6=;k<6s^WS&=zB`3MmiIuAM(qgmQl4kA4k%7#-8WiPw zbHc7NREM@|v=8?czn{ z_0{D|ORMY3Yq@o0>Dt=mnVlyfX~m7BD@ zvb?subn)W4c{6ves}XkF)TP~Gaj-HXyU}4$o}U*Uj62Zs9%ch-T#xj^Xe2*Y@Qg9y zV70S2s(Quu{|-dZVgPltC`hD14=6&_@)`Zd~;)w_(2(RDZAQuAFx z57i6|EgyShChC?`>yD7MgE5fO6*CGqD%Ahl17F1{b^Ab0i`9<@3{f zHVcEe(JuJ;5Pgeg|2(_T42tsHoN)gaEPpJD+$rnQ@8X%2`Z2hdvbjgq>HJEtcZC*j zYL(4u$HUt+Y5#{)`}u5*Oi`OHk>&-TC|46g^DW-lT#8b)?QONJ<@K_yY>jQq)wAZ$ww8bv}4H#|vjFWi6Mwo-Q;F zOdJo=aeR2nBfTg6wp5g+L*EJgO6Zl~PlMkG-U%j0J{=KPeV``~VYLv8cJQ`2UM`QDes1i@SkjTD} zTzozoi^pP#g|R`;1Z65{m3=crlj+6eg~OELGfQmVO$EeNt=ym$D;|=yf>!F5c2_Oy zmCeQt*P^4*bSC}sC}}$1(iBs7ws~}EyH>q>&$hx-chj(#O~f*>vl3~z)Y1?&Z81U# zY?iT)8Ha|s-)>XygVstSY^^%I&wu(JR zkW6NAF_T&tAf}Xq4+wh?Ihk}O5shU8LO1I~w|7a&Y$~1!O7D35(ru6QAJVs_Z%DVL zUzWZs-F=dWbod4WAOHd&00JNY0w4eaAOHd&00KWN0^>gKoZ#4IGZ_qcr-%4W9&H;r zv^Eu)|BI8p*%`sU z`N4MX5Ber1c*KAO0|rCB37KEFbgr0+;yqt*XUOS7RLhu#mp6Z%|eHS|pI$H9LM{%PyyVxCK40rEp(m!M1)PsxYL$9X2{WB;;3wP$QgH9a>~;rH!~-)o$)qh zblH-adx|9HGk$KBZL-p~%)F7T5mnXMG*r|6XkVJ_k2R#+3qwkg)F)AYWBu#oYZ_E-r_)-=kxd755r zVA7^#RsxeY&9iQvq?Z_|)q0tY)?Fsb47Kje(yI!>v#|kgZEHr`wv}!ue1CsLB(b^F zfN82-&@|K%rMC^3sA;EN)U>iKIzz7$2(t?V%t*T&GqEl79K9$In9pznZ4>OajpjXS zheiE)dLtkZd(pJgA#EDkAy4lEki2QCL*BI3E`N$%2MElin7s~RX0lz9t^ZF3zv-b> z|8GlwPBDNlNk1=bN>`;%NhhUH=zl`r4Sf)LH}v(;S3|!Z`fR8Yx*j?cIvJAaF9duy z_>Ytk9}oZm5C8!X009sH0T2KI5CDOn34t-6Fe`M#g(i>qgoxk>106SRwkD9qpR+i2 z*eA>g_5jb*M}5M)&=t^m>M5TP6I?fgwT$o93J%v(}FE@GRg^b#7ZRNmZ^jXJ3_o!3kerUT{Vh ze6lYP6S`vvUVh)yYMcJk`hQ?9;D`yZ^?&-K|M-9a2!H?xfB*=900@8p2!H?xfB*zF~>2IaK zl>S2c6Y1;HA4tC|eMR~m=}XeDN}rdS(p%DwRFz6nfimI)0w4eaAOHd&00JNY0w4ea zAOHgWPGHb02;w9^9_Pnn{P;9G`k&%QnIDhx;{-bn9^uDvejMY+!|doAFKmY_l00ck)1V8`;KmY_lpf3qv{@<6?4yQo?1V8`; zKmY_l00ck)1V8`;`iubP|9w{Ba2EtX00ck)1V8`;KmY_l00cmwF9~4&-+d0jEVA_WKw-^lemQ{*_KTvQqe-Rpzc&kx11J?n+9H6S-x^@ ze>>fk?r-&JCEUtN9=vc^lsB_NQ?ooUQn&TmR;gYuRV#JXI%iHS-v8`zQJ$X{KEKe= z@@2hvQ>$!NRjs&Hs;K4art|s}reAAIuUyP2VP`TUN-&jDrJ--?HD%@UnzC~3;zi~4 z)#Xb|tLw^Zxpigf+S=vi6;gC5x3WeaRP#6WLW8UcD-Hc_gJ-aA*YxdLSh=m$3OBUc zsdzLRX=^1RtNod}( zS`9aaAFS5mY&o%wd_J`iFSHFZI-M=Uv0k0@fSR4n-_9)32R4Sx6PKah+F1 zSC1casfI2g=dfUEv{gY@7B~EJqoVud@1kC^tDL!UF*?2p~Ocdwz3H)+=a^P~0owv*lF zbSdVt2OCWy!zVqj%TrUr+cTZW+At3FfYgId^UlPO+zc!Uy9-u};ZU5Nz5Ph<=<#YS zsipJjMBMQZcIJM{G^^L+*eQRI(rgXQ)neu{PoVmx;l0Cd8mY#Qi1HQsx{-1?ZZtlt zlVmfgR5q1Nwh^1(k&e1_4_;g^1x|YnIc6n4E*_`$5~hkGC;*)yVCY!!tRWPv#aS@J<6&? z_7I8|iivnCAGJo1uB@ZIM~^Pq0~KQ<-ld?ULw2>%$nNzKQC?aS9=vEZC|+UCTC>Ir zXG+ker@E?msNTvoJ%UGCN3FTcFuI;=R)z!i)z=67^4y%T&cW7il}im>t?RdT^h!Z@ zTzWeYIucJ+v@QKK<%~4f1yNpD5Oy`L)0{1Bl{RY{o3_-uZVYtjZB4JN3=dT!+V-|> z0+mX2OrV&py;CV`xuny?tb5*o^?yf91D7BG0w4eaAOHd&00JNY0w4eaAn*hd2zt+Z zMm_gEhh7T&{?NY-jrqSvtN&XAf9PHJeskb+!o7hrp8LW}Qt1)8-b}|tIT8_epS6cq z>a>hY!4)&MVqPh(ffX~6MODmG^-cYr6%I+-7Mtx$*c6s)$x~oN>s~R|?{1fBy2_#^ zZn2XW<9_*aMDXx%r5Qb)nNFQD>+2 zYsZR=Gx?z+N~|1`Ra;^cS&kH1=HQfE7{5*~_EXK*c+vNtD#Q^fmgp0O))%ng6c&-+T^ zv|m0yy{BVF(Ae;dpHCh7&=Wrhjr+-Y-eumNx&IuU?X{FAx}ade-G?Hvt<~#ysx^uv zwsvyF;)@-D$g);%upK9*ioLne1`BFRrMg<*p?I!Qe7;&OxA!klD!aQsX%gf`>>)`a z`j%EITSmrH$qpl#Aq{O~Bdj=$pcJ;z;x_dQXA&C))SJ|~T?469=tikVzVBgJr1?yk z7S`uF7S^r!wrbyR!qzn68_7&sPa3O5mR!iJQTwhbE48N<+iHa+&02w@SX$J)IxWig z=7e3zT8Oc-bQQpc5$3fTjcNHrCZ$sc#?)$Yl(Vtc~A*U~hry~D4)99&4H zlX_lDI|U|9x6Dz)ncS7X19rO@=6Y*sFw#6VDatBsez;<9DQ(&A-ki)A^hG`IEc^IA zrrA3I8y5?vp57)pa)fCC-?xkXk=P&kgzS&}f4uReU$=@ik0eC-YD8#e?So}&{%Ma$j;GTJbC_(+Ze0h-z4CVqk1Z`Ue%s0lN3j0?2qPO%4FVtl0w4eaAOHd&00JNY z0w4eaj|l(qQK!gFes1 z1pAR9_UkJ_A6?1pivMibn3pc6*`@UpMTZA`9{B|SEf)GUF49uFH!hIa4EqH(^Echt zZzHk!Km7n8J|F-BAOHd&00JNY0w4eaAOHd&00K`i0o(q6=?Qidu7LmufB*=900@8p z2!H?xfB*=9KwlHU`hQKq<}00@8p2!H?xfB*=900@8p2=pI;{{sa>FjfEn literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1f80496 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/forms/__init__.py b/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forms/admin.py b/forms/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/forms/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/forms/apps.py b/forms/apps.py new file mode 100644 index 0000000..086edd3 --- /dev/null +++ b/forms/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FormsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'forms' diff --git a/forms/forms.py b/forms/forms.py new file mode 100644 index 0000000..0be5083 --- /dev/null +++ b/forms/forms.py @@ -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) diff --git a/forms/migrations/0001_initial.py b/forms/migrations/0001_initial.py new file mode 100644 index 0000000..93cb759 --- /dev/null +++ b/forms/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/forms/migrations/__init__.py b/forms/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forms/models.py b/forms/models.py new file mode 100644 index 0000000..b1e928c --- /dev/null +++ b/forms/models.py @@ -0,0 +1,17 @@ +from django.db import models + +class FormResponse(models.Model): + 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(null=True, blank=True) # ����� ������� + downtime_reason = models.TextField() # ������� ������� + fix_method = models.TextField() # ������ ���������� �������� + + created_at = models.DateTimeField(auto_now_add=True) # ����� �������� ������ + + def __str__(self): + return f"{self.line} - {self.problem} ({self.occurred_at})" diff --git a/forms/serializers.py b/forms/serializers.py new file mode 100644 index 0000000..625614f --- /dev/null +++ b/forms/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers +from .models import FormResponse + +class FormResponseSerializer(serializers.ModelSerializer): + class Meta: + model = FormResponse + fields = '__all__' + + diff --git a/forms/tests.py b/forms/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/forms/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/forms/urls.py b/forms/urls.py new file mode 100644 index 0000000..696eab6 --- /dev/null +++ b/forms/urls.py @@ -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-запрос +] diff --git a/forms/views.py b/forms/views.py new file mode 100644 index 0000000..63fcae2 --- /dev/null +++ b/forms/views.py @@ -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) \ No newline at end of file diff --git a/inventory/__init__.py b/inventory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inventory/admin.py b/inventory/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/inventory/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/inventory/apps.py b/inventory/apps.py new file mode 100644 index 0000000..905749f --- /dev/null +++ b/inventory/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InventoryConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'inventory' diff --git a/inventory/migrations/0001_initial.py b/inventory/migrations/0001_initial.py new file mode 100644 index 0000000..b43bad5 --- /dev/null +++ b/inventory/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/inventory/migrations/__init__.py b/inventory/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inventory/models.py b/inventory/models.py new file mode 100644 index 0000000..d9b3394 --- /dev/null +++ b/inventory/models.py @@ -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() diff --git a/inventory/serializers.py b/inventory/serializers.py new file mode 100644 index 0000000..e3f3e3f --- /dev/null +++ b/inventory/serializers.py @@ -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 \ No newline at end of file diff --git a/inventory/serializers_transfer.py b/inventory/serializers_transfer.py new file mode 100644 index 0000000..10d2701 --- /dev/null +++ b/inventory/serializers_transfer.py @@ -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 diff --git a/inventory/tests.py b/inventory/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/inventory/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/inventory/urls.py b/inventory/urls.py new file mode 100644 index 0000000..4cef6b4 --- /dev/null +++ b/inventory/urls.py @@ -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 + } + ] +} +""" \ No newline at end of file diff --git a/inventory/views.py b/inventory/views.py new file mode 100644 index 0000000..7392810 --- /dev/null +++ b/inventory/views.py @@ -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 + diff --git a/inventory/views_transfer.py b/inventory/views_transfer.py new file mode 100644 index 0000000..69c2d41 --- /dev/null +++ b/inventory/views_transfer.py @@ -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": "Отклонена"}) diff --git a/issues/__init__.py b/issues/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/issues/admin.py b/issues/admin.py new file mode 100644 index 0000000..263c876 --- /dev/null +++ b/issues/admin.py @@ -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",) diff --git a/issues/apps.py b/issues/apps.py new file mode 100644 index 0000000..2722971 --- /dev/null +++ b/issues/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class IssuesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'issues' diff --git a/issues/migrations/0001_initial.py b/issues/migrations/0001_initial.py new file mode 100644 index 0000000..cbca225 --- /dev/null +++ b/issues/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/issues/migrations/__init__.py b/issues/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/issues/models.py b/issues/models.py new file mode 100644 index 0000000..283d9d1 --- /dev/null +++ b/issues/models.py @@ -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// – удалить проблему""" + + + +"""Модели справочника номенклатура и сообщения""" +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]}" \ No newline at end of file diff --git a/issues/serializers.py b/issues/serializers.py new file mode 100644 index 0000000..f43c064 --- /dev/null +++ b/issues/serializers.py @@ -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__' \ No newline at end of file diff --git a/issues/tests.py b/issues/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/issues/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/issues/urls.py b/issues/urls.py new file mode 100644 index 0000000..6aeecb5 --- /dev/null +++ b/issues/urls.py @@ -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)), +] diff --git a/issues/views.py b/issues/views.py new file mode 100644 index 0000000..1da2efd --- /dev/null +++ b/issues/views.py @@ -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 \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..f237a58 --- /dev/null +++ b/manage.py @@ -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() diff --git a/mobileApps/fletFormTest.py b/mobileApps/fletFormTest.py new file mode 100644 index 0000000..82410e2 --- /dev/null +++ b/mobileApps/fletFormTest.py @@ -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) diff --git a/production/__init__.py b/production/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/production/admin.py b/production/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/production/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/production/apps.py b/production/apps.py new file mode 100644 index 0000000..71b3232 --- /dev/null +++ b/production/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProductionConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'production' diff --git a/production/migrations/0001_initial.py b/production/migrations/0001_initial.py new file mode 100644 index 0000000..6c31ed2 --- /dev/null +++ b/production/migrations/0001_initial.py @@ -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='Создано')), + ], + ), + ] diff --git a/production/migrations/__init__.py b/production/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/production/models.py b/production/models.py new file mode 100644 index 0000000..80bcfc4 --- /dev/null +++ b/production/models.py @@ -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}" diff --git a/production/serializers.py b/production/serializers.py new file mode 100644 index 0000000..288f06a --- /dev/null +++ b/production/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import ProductionPlan + +class ProductionPlanSerializer(serializers.ModelSerializer): + class Meta: + model = ProductionPlan + fields = '__all__' diff --git a/production/tests.py b/production/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/production/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/production/urls.py b/production/urls.py new file mode 100644 index 0000000..3ee0a75 --- /dev/null +++ b/production/urls.py @@ -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)), +] diff --git a/production/views.py b/production/views.py new file mode 100644 index 0000000..9983c47 --- /dev/null +++ b/production/views.py @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..c38d0a8e75fed55d5795985db8da69b662725b84 GIT binary patch literal 6426 zcma)=U2l^|5QX=;Qh!Pic0z!QUQi(}P*JHsqP>b7+i?v3!gffUAK&&obKc3k4pgcv z^WmADojG%6c6R^yd(`G_-LBiHZS*;`X?!pB|Ajug_KQBl_N1NZd)j_(KS|;h`?l8# zdH)y+PPO*E$PXn6+)}eI1r;)kJJK0^!?o_*g&9;s8 z|4ID+O8?sGchK7l$+i4CY2RdTq`z0v*@w+xBZ9ODdfNwl*$C)c3*VsGe^!Ml2O>0E9RTX^;&;#v_~Q|2%l1a9*iKWts+ky zymm~NlF#Ms7g^qhoP3u*V|fEo!;pc+`9n10@RN0i2Pv@{52RZmf~WJ44<#9PXe1(I zNmI_wgRq|#U^t8c53|}>)*iB8|<7mpG8kuGtg z-?x}3uJt>Sj3SoQ0Lk;tOW40t%=omIw(~jc^$1rs^@9d6Y@>FtmK`GZgAbd~sy<bR~hGF>@DpRc`kHQs5}B6{wZ2z56bVpiYRW7y*Z#^=h5`PxYa* z%!H}?f%%=gptBB*RrpGT8M~_iM6curryccxv^woe?V0$HHR`s|!(&Tl`gN_SH(F~8 zPg@n<}LD$hwsKq?1>>)^1 z1aq;CsRbFia>nVM$9faO*k{w+VxyE9c!}Iky>t^I?jy2e3;8NjHic! zCV9KjFNtRH`K@qXMdvZrW>$8!&syhG`7+iLC7#;LFIs!6FTO5RJ=r-^%b0(WbSW!0 zVFCV^1u(qSdbn%vh-0Wv=5*0qYk2NHlx$4Ng*xOu>hVJ8QYrn@3}1{1XXjQGLf5!e z%qQBLla&sXd8!P)>$I6?KIq?9vYY5()7`_f6Mc?3($SG$ulkz@?WF?BlMWL$kj}U z7$s}GjMJIyaWZaXh3WwXw0s&AMu*77N<2tE&2Rfq(hFh0t&582-}20%)Mh_=1w8c} zXOvxy_~gz?TzSSN2a{XJI&`KwlWYy9^euuV-jlWJkngqIOkVrEC>{<1gXAgq(wv%M zQ~GTbs;lAq_nOR@%m!Z+u_sT?rmZxyQx$W;2%p}zPwiv)OEh&lvx8YUk=OgKqVd3N zOnx2O??RcVKIrpXXUTc!=QT5d*RIg4Vq=Fd@7wG4rpF)OI*m={bn~#@EG8l*ljrga zR@yJD{UNWc1D{G|;a%a?`KIV|^G$2EIkK@!% zP4yD+$u;z=oFQNmb?d2@YDdN!nC}U7Iv8Q}jnr22;!arw$;>P{%3T(+LM)t}F!;Ul zg&v4t%k150KltS7%iY14!4S9)lAu~|6RsqC@wkE{!0DhI@%$4C=N-m*}Ivi!+f2!X<(Le^ZG_?fYZm7aoSZEcp=YMDkG7#`Aw@XRlVHJtB2DCYF@fk zRQt$zPOpy4`ABBpN`t%KBqGVI<7wUpZnX<{JHJ}M!L;L=!_!aF;ZDY@1FwtNA-nE4 zXfV6yYjNV>9Q-NCPV!HZQ_(r|Bs+{h>+}67E6f$hRG9ZIB%TAzO8lXxfKHW#w SrqTyv@5)OKJqGQX>iz{kY@LAs literal 0 HcmV?d00001 diff --git a/scan/__init__.py b/scan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scan/admin.py b/scan/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/scan/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/scan/api_client.py b/scan/api_client.py new file mode 100644 index 0000000..8739087 --- /dev/null +++ b/scan/api_client.py @@ -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) \ No newline at end of file diff --git a/scan/apps.py b/scan/apps.py new file mode 100644 index 0000000..a4f8e92 --- /dev/null +++ b/scan/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ScanConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'scan' diff --git a/scan/celery_app.py b/scan/celery_app.py new file mode 100644 index 0000000..b794cd0 --- /dev/null +++ b/scan/celery_app.py @@ -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() diff --git a/scan/constants.py b/scan/constants.py new file mode 100644 index 0000000..0548bc4 --- /dev/null +++ b/scan/constants.py @@ -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" +} \ No newline at end of file diff --git a/scan/migrations/0001_initial.py b/scan/migrations/0001_initial.py new file mode 100644 index 0000000..71afd54 --- /dev/null +++ b/scan/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/scan/migrations/__init__.py b/scan/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scan/models.py b/scan/models.py new file mode 100644 index 0000000..bf63b27 --- /dev/null +++ b/scan/models.py @@ -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) + diff --git a/scan/serializers.py b/scan/serializers.py new file mode 100644 index 0000000..a78fe1e --- /dev/null +++ b/scan/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers +from .models import ScannedCode + +class ScannedCodeSerializer(serializers.ModelSerializer): + class Meta: + model = ScannedCode + fields = "__all__" diff --git a/scan/tasks.py b/scan/tasks.py new file mode 100644 index 0000000..7493860 --- /dev/null +++ b/scan/tasks.py @@ -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 diff --git a/scan/tests.py b/scan/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/scan/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/scan/urls.py b/scan/urls.py new file mode 100644 index 0000000..e258af4 --- /dev/null +++ b/scan/urls.py @@ -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'), +] diff --git a/scan/utils.py b/scan/utils.py new file mode 100644 index 0000000..f3ed7af --- /dev/null +++ b/scan/utils.py @@ -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) \ No newline at end of file diff --git a/scan/views.py b/scan/views.py new file mode 100644 index 0000000..1671c76 --- /dev/null +++ b/scan/views.py @@ -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 + ) \ No newline at end of file diff --git a/scan/viewss1/__init__.py b/scan/viewss1/__init__.py new file mode 100644 index 0000000..80873f5 --- /dev/null +++ b/scan/viewss1/__init__.py @@ -0,0 +1,3 @@ +# views/__init__.py +from .scan_views import StorageScanView, AspuScanView +from .management_views import ManagementDataView \ No newline at end of file diff --git a/scan/viewss1/management_views.py b/scan/viewss1/management_views.py new file mode 100644 index 0000000..7b02abd --- /dev/null +++ b/scan/viewss1/management_views.py @@ -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" + ) diff --git a/scan/viewss1/scan_views.py b/scan/viewss1/scan_views.py new file mode 100644 index 0000000..4dbb42b --- /dev/null +++ b/scan/viewss1/scan_views.py @@ -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) \ No newline at end of file diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..72b1401 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/users/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/users/serializers.py b/users/serializers.py new file mode 100644 index 0000000..824acb7 --- /dev/null +++ b/users/serializers.py @@ -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) + } diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..5917abd --- /dev/null +++ b/users/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import UserLoginView + +urlpatterns = [ + path("login/", UserLoginView.as_view(), name="user-login"), +] diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..f21f658 --- /dev/null +++ b/users/views.py @@ -0,0 +1,11 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from .serializers import UserLoginSerializer + +class UserLoginView(APIView): + def post(self, request): + serializer = UserLoginSerializer(data=request.data) + if serializer.is_valid(): + return Response(serializer.validated_data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)