Загрузка проекта barcode
This commit is contained in:
commit
0cbc52b7da
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Используем официальный образ Python
|
||||||
|
FROM python:3.10
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию в контейнере
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем файлы проекта в контейнер
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# Устанавливаем зависимости
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Применяем миграции
|
||||||
|
RUN python manage.py migrate
|
||||||
|
|
||||||
|
# Запускаем сервер Django
|
||||||
|
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .celery_app import app as celery_app
|
||||||
1
barcode/__init__.py
Normal file
1
barcode/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .celery_app import app as celery_app
|
||||||
BIN
barcode/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
barcode/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
barcode/__pycache__/celery_app.cpython-312.pyc
Normal file
BIN
barcode/__pycache__/celery_app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
barcode/__pycache__/settings.cpython-312.pyc
Normal file
BIN
barcode/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
BIN
barcode/__pycache__/urls.cpython-312.pyc
Normal file
BIN
barcode/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
barcode/__pycache__/wsgi.cpython-312.pyc
Normal file
BIN
barcode/__pycache__/wsgi.cpython-312.pyc
Normal file
Binary file not shown.
20
barcode/asgi.py
Normal file
20
barcode/asgi.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for barcode project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from barcode.routing import websocket_urlpatterns
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "barcode.settings")
|
||||||
|
|
||||||
|
application = ProtocolTypeRouter({
|
||||||
|
"http": get_asgi_application(),
|
||||||
|
"websocket": URLRouter(websocket_urlpatterns),
|
||||||
|
})
|
||||||
11
barcode/celery_app.py
Normal file
11
barcode/celery_app.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
# Óęŕçűâŕĺě ďóňü ę íŕńňđîéęŕě Django
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "barcode.settings")
|
||||||
|
|
||||||
|
app = Celery("barcode")
|
||||||
|
|
||||||
|
# Çŕăđóćŕĺě íŕńňđîéęč Celery čç Django
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
20
barcode/consumers.py
Normal file
20
barcode/consumers.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
from .data_fetcher import fetch_lines_data # Вынесем логику получения данных в отдельный модуль
|
||||||
|
|
||||||
|
class LineDataConsumer(AsyncWebsocketConsumer):
|
||||||
|
async def connect(self):
|
||||||
|
await self.accept()
|
||||||
|
self.running = True
|
||||||
|
await self.send(text_data=json.dumps({"message": "WebSocket connected"}))
|
||||||
|
asyncio.create_task(self.send_data_periodically())
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
async def send_data_periodically(self):
|
||||||
|
while self.running:
|
||||||
|
data = await fetch_lines_data()
|
||||||
|
await self.send(text_data=json.dumps(data))
|
||||||
|
await asyncio.sleep(5) # Ждем 5 секунд перед следующим запросом
|
||||||
63
barcode/data_fetcher.py
Normal file
63
barcode/data_fetcher.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
LOGIN_URL_ASPU = "http://192.168.254.10:3428/api/login"
|
||||||
|
|
||||||
|
API_URLS = {
|
||||||
|
"Sipa": "http://192.168.254.10:3428/api/Management/",
|
||||||
|
"Devin": "http://192.168.254.20:3428/api/Management/",
|
||||||
|
"JR": "http://192.168.254.30:3428/api/Management/",
|
||||||
|
"5L": "http://192.168.254.40:3428/api/Management/",
|
||||||
|
"19L": "http://192.168.254.50:3428/api/Management/",
|
||||||
|
}
|
||||||
|
|
||||||
|
USERNAME = "superuser"
|
||||||
|
PASSWORD = "Superuser1105"
|
||||||
|
|
||||||
|
token_cache = {"token": None, "timestamp": 0}
|
||||||
|
TOKEN_LIFETIME = 1440 * 60 # 24 часа в секундах
|
||||||
|
|
||||||
|
def get_new_token():
|
||||||
|
"""Получение нового токена с кешированием."""
|
||||||
|
global token_cache
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if token_cache["token"] and (current_time - token_cache["timestamp"] < TOKEN_LIFETIME):
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
payload = {"UserName": USERNAME, "Password": PASSWORD}
|
||||||
|
try:
|
||||||
|
response = requests.post(LOGIN_URL_ASPU, json=payload, timeout=5)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and response_data.get("IsSuccess"):
|
||||||
|
token_cache["token"] = response_data["Value"]["Token"]
|
||||||
|
token_cache["timestamp"] = current_time
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
logging.error(f"Ошибка получения токена: {response_data}")
|
||||||
|
return None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка сети при получении токена: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def fetch_lines_data():
|
||||||
|
"""Асинхронное получение данных с линий."""
|
||||||
|
token = get_new_token()
|
||||||
|
if not token:
|
||||||
|
return {"error": "Не удалось получить токен"}
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for key, url in API_URLS.items():
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers, timeout=5)
|
||||||
|
response.raise_for_status()
|
||||||
|
results[key] = response.json()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка запроса к {key}: {str(e)}")
|
||||||
|
results[key] = {"error": str(e)}
|
||||||
|
|
||||||
|
return results
|
||||||
6
barcode/routing.py
Normal file
6
barcode/routing.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import re_path
|
||||||
|
from .consumers import LineDataConsumer
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
re_path(r"ws/lines/$", LineDataConsumer.as_asgi()),
|
||||||
|
]
|
||||||
203
barcode/settings.py
Normal file
203
barcode/settings.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
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',
|
||||||
|
"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': 'mydatabase',
|
||||||
|
'USER': 'myuser',
|
||||||
|
'PASSWORD': 'mypassword',
|
||||||
|
'HOST': '31.130.144.182',
|
||||||
|
'PORT': '5432',
|
||||||
|
'OPTIONS': {
|
||||||
|
'client_encoding': 'UTF8',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> UTF-8 <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer', # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> JSON
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
CORS_ALLOW_HEADERS = ['Content-Type'] # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
# <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> localhost:3000
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:3000"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
'ACCESS_TOKEN_LIFETIME': timedelta(days=1), # Токен живет 1 день
|
||||||
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh-токен живет 7 дней
|
||||||
|
'AUTH_HEADER_TYPES': ('Bearer',), # Токен передается через заголовок Authorization
|
||||||
|
}
|
||||||
|
|
||||||
|
ASGI_APPLICATION = "your_project.asgi.application"
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "channels.layers.InMemoryChannelLayer",
|
||||||
|
"CONFIG": {
|
||||||
|
"capacity": 1000, # Увеличь лимит соединений
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
33
barcode/urls.py
Normal file
33
barcode/urls.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
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")),
|
||||||
|
]
|
||||||
16
barcode/wsgi.py
Normal file
16
barcode/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for barcode project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'barcode.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
0
batches/__init__.py
Normal file
0
batches/__init__.py
Normal file
BIN
batches/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
batches/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
batches/__pycache__/admin.cpython-312.pyc
Normal file
BIN
batches/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
batches/__pycache__/apps.cpython-312.pyc
Normal file
BIN
batches/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
batches/__pycache__/models.cpython-312.pyc
Normal file
BIN
batches/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
batches/__pycache__/urls.cpython-312.pyc
Normal file
BIN
batches/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
batches/__pycache__/views.cpython-312.pyc
Normal file
BIN
batches/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
batches/admin.py
Normal file
3
batches/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
batches/apps.py
Normal file
6
batches/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BatchesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'batches'
|
||||||
0
batches/migrations/__init__.py
Normal file
0
batches/migrations/__init__.py
Normal file
BIN
batches/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
batches/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
3
batches/models.py
Normal file
3
batches/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
3
batches/tests.py
Normal file
3
batches/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
14
batches/urls.py
Normal file
14
batches/urls.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.urls import path
|
||||||
|
# from .views import ScanAPIView, ScanAspuAPIView, GetManagementAPIView
|
||||||
|
from .views import BatchListView, BatchListGTIN, BatchListReport, BatchReportView
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# path('api/scan/', ScanAPIView.as_view(), name='scan'),
|
||||||
|
# path('api/scan_aspu/', ScanAspuAPIView.as_view(), name='scan_aspu'),
|
||||||
|
path('batches/', BatchListView.as_view(), name='batch-list'),
|
||||||
|
path('gtin/', BatchListGTIN.as_view(), name='batch-gtin'),
|
||||||
|
path('reports/', BatchListReport.as_view(), name='batch-gtin'),
|
||||||
|
path('reports-view/', BatchReportView.as_view(), name='batch-gtin')
|
||||||
|
|
||||||
|
]
|
||||||
193
batches/views.py
Normal file
193
batches/views.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import requests
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.conf import settings
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
|
||||||
|
# http://192.168.254.2/szkm/api/batch/load/eb7bb39a-fd68-47c7-aec8-0041a4fe5faf/
|
||||||
|
|
||||||
|
# Настройки API
|
||||||
|
API_BASE_URL = "http://192.168.254.2:8280/api"
|
||||||
|
API_REPORTS_URL = f"{API_BASE_URL}/productlinereport/list"
|
||||||
|
LOGIN_URL = f"{API_BASE_URL}/login"
|
||||||
|
BATCH_LIST_URL = f"{API_BASE_URL}/Batch/list"
|
||||||
|
|
||||||
|
|
||||||
|
# Данные для входа
|
||||||
|
USERNAME = "user"
|
||||||
|
PASSWORD = "user"
|
||||||
|
|
||||||
|
def get_auth_token():
|
||||||
|
"""Авторизуется и получает Bearer токен"""
|
||||||
|
response = requests.post(LOGIN_URL, json={"username": USERNAME, "password": PASSWORD})
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
token = data.get("Value", {}).get("Token") # Исправленный способ получения токена
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BatchListView(View):
|
||||||
|
"""Получает список партий с фильтрацией по CreatedOn"""
|
||||||
|
def get(self, request):
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"Filter": {
|
||||||
|
"Filters": [
|
||||||
|
{"Value": "2024-01-1 00:00:00", "Operator": "gte", "Field": "CreatedOn"},
|
||||||
|
{"Value": "2025.11.22", "Operator": "lte", "Field": "CreatedOn"}
|
||||||
|
],
|
||||||
|
"Logic": "and"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(BATCH_LIST_URL, json=payload, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return JsonResponse(response.json(), safe=False)
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе данных"}, status=response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchListGTIN(View):
|
||||||
|
"""Получает список партий с фильтрацией по CreatedOn"""
|
||||||
|
def get(self, request):
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"Filter": {
|
||||||
|
"Filters": [
|
||||||
|
{
|
||||||
|
"Filters": [
|
||||||
|
{
|
||||||
|
"Value": "04607017161371",
|
||||||
|
"Operator": "eq",
|
||||||
|
"Field": "ProductType.GTIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Value": "04607017161371",
|
||||||
|
"Operator": "linq",
|
||||||
|
"Field": "CodesForProductLines.Any( a=> a.ProductType.GTIN=='{0}' )"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Logic": "or"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Logic": "and"
|
||||||
|
},
|
||||||
|
"Includes": [
|
||||||
|
"ProductType",
|
||||||
|
"CodesForProductLines.ProductType"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(BATCH_LIST_URL, json=payload, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return JsonResponse(response.json(), safe=False)
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе данных"}, status=response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class BatchListReport(View):
|
||||||
|
"""Получает список партий с фильтрацией по CreatedOn и BatchId из тела запроса"""
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = json.loads(request.body)
|
||||||
|
batch_id = body.get("id")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({"error": "Неверный формат JSON"}, status=400)
|
||||||
|
|
||||||
|
if not batch_id:
|
||||||
|
return JsonResponse({"error": "Отсутствует параметр 'id'"}, status=400)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
payload = {"skip":0,"take":10,"includes":["ProductLineBatch","ProductLineBatch.ProductType","ProductLine"],"sort":[{"field":"CreatedOn","dir":"desc"}],"filter":{"filters":[{"operator":"eq","field":"ProductLineBatchId","value":batch_id}],"logic":"and"}}
|
||||||
|
|
||||||
|
response = requests.post(API_REPORTS_URL, json=payload, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return JsonResponse(response.json(), safe=False)
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе данных"}, status=response.status_code)
|
||||||
|
class BatchReportView(View):
|
||||||
|
"""Получает список партий и прикрепляет к ним отчёты асинхронно."""
|
||||||
|
|
||||||
|
async def fetch_reports(self, session, headers, batch_id):
|
||||||
|
"""Асинхронно получает список отчётов для заданной партии."""
|
||||||
|
if not batch_id:
|
||||||
|
return []
|
||||||
|
|
||||||
|
report_payload = {
|
||||||
|
"Skip": 0,
|
||||||
|
"Take": 100000,
|
||||||
|
"Filter": {
|
||||||
|
"Field": "BatchId",
|
||||||
|
"Operator": "eq",
|
||||||
|
"Value": batch_id
|
||||||
|
},
|
||||||
|
"TotalCount": 0,
|
||||||
|
"Sort": [{"Field": "CreatedOn", "Dir": "desc"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.post(API_REPORTS_URL, json=report_payload, headers=headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
return data.get("Value", {}).get("Items", [])
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def fetch_all_reports(self, batches, headers):
|
||||||
|
"""Запрашивает отчёты для всех партий параллельно."""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
tasks = [self.fetch_reports(session, headers, batch.get("ProductTypeId")) for batch in batches]
|
||||||
|
reports = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# Присваиваем отчёты соответствующим партиям
|
||||||
|
for batch, report in zip(batches, reports):
|
||||||
|
batch["Reports"] = report
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""Основной метод обработки GET-запроса."""
|
||||||
|
token = get_auth_token()
|
||||||
|
if not token:
|
||||||
|
return JsonResponse({"error": "Не удалось получить токен"}, status=401)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
# Запрос списка партий
|
||||||
|
batch_payload = {
|
||||||
|
"Filter": {
|
||||||
|
"Filters": [
|
||||||
|
{"Value": "2025-01-1 00:00:00", "Operator": "gte", "Field": "CreatedOn"},
|
||||||
|
{"Value": "2025.11.22", "Operator": "lte", "Field": "CreatedOn"}
|
||||||
|
],
|
||||||
|
"Logic": "and"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
batch_response = requests.post(BATCH_LIST_URL, json=batch_payload, headers=headers)
|
||||||
|
|
||||||
|
if batch_response.status_code != 200:
|
||||||
|
return JsonResponse({"error": "Ошибка при запросе списка партий"}, status=batch_response.status_code)
|
||||||
|
|
||||||
|
batch_data = batch_response.json()
|
||||||
|
batches = batch_data.get("Value", {}).get("Items", [])
|
||||||
|
print(batches)
|
||||||
|
# Асинхронно получаем отчёты для всех партий
|
||||||
|
asyncio.run(self.fetch_all_reports(batches, headers))
|
||||||
|
print(fetch_all_reports)
|
||||||
|
return JsonResponse({"Batches": batches}, safe=False)
|
||||||
|
|
||||||
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
django:
|
||||||
|
build: .
|
||||||
|
container_name: django_app
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- DEBUG=True
|
||||||
|
- DATABASE_URL=postgres://myuser:mypassword@db:5432/mydatabase
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: postgres_db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: myuser
|
||||||
|
POSTGRES_PASSWORD: mypassword
|
||||||
|
POSTGRES_DB: mydatabase
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
0
forms/__init__.py
Normal file
0
forms/__init__.py
Normal file
BIN
forms/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
forms/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
forms/__pycache__/admin.cpython-312.pyc
Normal file
BIN
forms/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
forms/__pycache__/apps.cpython-312.pyc
Normal file
BIN
forms/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
forms/__pycache__/models.cpython-312.pyc
Normal file
BIN
forms/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
forms/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
forms/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
forms/__pycache__/urls.cpython-312.pyc
Normal file
BIN
forms/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
forms/__pycache__/views.cpython-312.pyc
Normal file
BIN
forms/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
forms/admin.py
Normal file
3
forms/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
forms/apps.py
Normal file
6
forms/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FormsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'forms'
|
||||||
16
forms/forms.py
Normal file
16
forms/forms.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
class Step1Form(forms.Form):
|
||||||
|
линия = forms.CharField(label="Линия", max_length=100)
|
||||||
|
проблема = forms.CharField(label="Проблема", widget=forms.Textarea)
|
||||||
|
|
||||||
|
class Step2Form(forms.Form):
|
||||||
|
время_начала = forms.DateTimeField(label="Время начала проблемы", widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}))
|
||||||
|
время_решения = forms.DateTimeField(label="Время решения проблемы", widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}))
|
||||||
|
|
||||||
|
class Step3Form(forms.Form):
|
||||||
|
причина = forms.CharField(label="Причина простоя", widget=forms.Textarea)
|
||||||
|
устранение = forms.CharField(label="Способ устранения проблемы", widget=forms.Textarea)
|
||||||
30
forms/migrations/0001_initial.py
Normal file
30
forms/migrations/0001_initial.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-06 04:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FormResponse',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('factory', models.TextField()),
|
||||||
|
('line', models.CharField(max_length=100)),
|
||||||
|
('operators_name', models.TextField()),
|
||||||
|
('problem', models.TextField()),
|
||||||
|
('error_zone', models.TextField()),
|
||||||
|
('occurred_at', models.DateTimeField()),
|
||||||
|
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('downtime_reason', models.TextField()),
|
||||||
|
('fix_method', models.TextField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
forms/migrations/__init__.py
Normal file
0
forms/migrations/__init__.py
Normal file
BIN
forms/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
forms/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
forms/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
forms/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
17
forms/models.py
Normal file
17
forms/models.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class FormResponse(models.Model):
|
||||||
|
factory = models.TextField()
|
||||||
|
line = models.CharField(max_length=100) # <20><><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
operators_name = models.TextField() # <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
problem = models.TextField() # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
error_zone = models.TextField() # <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
occurred_at = models.DateTimeField() # <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
resolved_at = models.DateTimeField(null=True, blank=True) # <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
downtime_reason = models.TextField() # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
fix_method = models.TextField() # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True) # <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.line} - {self.problem} ({self.occurred_at})"
|
||||||
9
forms/serializers.py
Normal file
9
forms/serializers.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import FormResponse
|
||||||
|
|
||||||
|
class FormResponseSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = FormResponse
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
3
forms/tests.py
Normal file
3
forms/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
8
forms/urls.py
Normal file
8
forms/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import FormResponseCreateView, FormResponseListView, FormResponseUploadView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('submit/', FormResponseCreateView.as_view(), name='submit_response'), # POST-запрос
|
||||||
|
path('submit/uploads/', FormResponseUploadView.as_view(), name='form-upload-report'),
|
||||||
|
path('responses/', FormResponseListView.as_view(), name='list_responses'), # GET-запрос
|
||||||
|
]
|
||||||
90
forms/views.py
Normal file
90
forms/views.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
from .serializers import FormResponseSerializer
|
||||||
|
import pandas as pd
|
||||||
|
from rest_framework import generics, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from .models import FormResponse
|
||||||
|
import logging
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
import pandas as pd
|
||||||
|
from .models import FormResponse # Убедись, что модель импортирована
|
||||||
|
from .serializers import FormResponseSerializer # Импортируем сериализатор
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
# API для сохранения ответов
|
||||||
|
class FormResponseCreateView(generics.CreateAPIView):
|
||||||
|
queryset = FormResponse.objects.all()
|
||||||
|
serializer_class = FormResponseSerializer
|
||||||
|
|
||||||
|
# API для получения списка ответов
|
||||||
|
class FormResponseListView(generics.ListAPIView):
|
||||||
|
queryset = FormResponse.objects.all()
|
||||||
|
serializer_class = FormResponseSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# API для загрузки Excel-файла и сохранения в БД
|
||||||
|
logger = logging.getLogger(__name__) # Логирование ошибок
|
||||||
|
|
||||||
|
|
||||||
|
class FormResponseUploadView(APIView):
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
print("\n===== [DEBUG] Данные, полученные на сервере =====")
|
||||||
|
print(json.dumps(request.data, indent=4, ensure_ascii=False)) # Логируем JSON
|
||||||
|
|
||||||
|
if "data" not in request.data:
|
||||||
|
return Response({"error": "Данные не найдены в запросе"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
uploaded_data = request.data["data"]
|
||||||
|
responses = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for item in uploaded_data:
|
||||||
|
# Обработка значений по умолчанию
|
||||||
|
operators_name = item.get("operators_name", "Анонимный пользователь") or "Анонимный пользователь"
|
||||||
|
error_zone = item.get("error_zone", "Не указано") or "Не указано"
|
||||||
|
downtime_reason = item.get("downtime_reason", "Не указано") or "Не указано"
|
||||||
|
fix_method = item.get("fix_method", "Не указано") or "Не указано"
|
||||||
|
|
||||||
|
# Функция для парсинга дат
|
||||||
|
def parse_date(date_str, default_now=False):
|
||||||
|
if not date_str:
|
||||||
|
return datetime.utcnow().isoformat() if default_now else None # Подставляем текущую дату если default_now=True
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(date_str.replace("Z", "+00:00")).isoformat()
|
||||||
|
except ValueError:
|
||||||
|
errors.append({"occurred_at": f"Неверный формат даты: {date_str}"})
|
||||||
|
return None
|
||||||
|
|
||||||
|
occurred_at = parse_date(item.get("occurred_at"), default_now=True) # Если нет даты — ставим текущее время
|
||||||
|
resolved_at = parse_date(item.get("resolved_at")) # Если нет, оставляем None
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
"line": item.get("line", "Неизвестная линия"),
|
||||||
|
"operators_name": operators_name,
|
||||||
|
"problem": item.get("problem", "Не указано"),
|
||||||
|
"error_zone": error_zone,
|
||||||
|
"occurred_at": occurred_at,
|
||||||
|
"resolved_at": resolved_at,
|
||||||
|
"downtime_reason": downtime_reason,
|
||||||
|
"fix_method": fix_method,
|
||||||
|
}
|
||||||
|
|
||||||
|
serializer = FormResponseSerializer(data=response_data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
responses.append(serializer.save())
|
||||||
|
else:
|
||||||
|
errors.append(serializer.errors)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n===== [DEBUG] Ошибки при сохранении данных =====")
|
||||||
|
print(json.dumps(errors, indent=4, ensure_ascii=False)) # Логируем ошибки
|
||||||
|
return Response({"error": "Некоторые записи не были сохранены", "details": errors}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
return Response({"message": "Данные успешно загружены!", "saved": len(responses)}, status=status.HTTP_201_CREATED)
|
||||||
0
inventory/__init__.py
Normal file
0
inventory/__init__.py
Normal file
BIN
inventory/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
inventory/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
inventory/__pycache__/admin.cpython-312.pyc
Normal file
BIN
inventory/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
inventory/__pycache__/apps.cpython-312.pyc
Normal file
BIN
inventory/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
inventory/__pycache__/models.cpython-312.pyc
Normal file
BIN
inventory/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
inventory/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
inventory/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
inventory/__pycache__/urls.cpython-312.pyc
Normal file
BIN
inventory/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
inventory/__pycache__/views.cpython-312.pyc
Normal file
BIN
inventory/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
inventory/admin.py
Normal file
3
inventory/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
inventory/apps.py
Normal file
6
inventory/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'inventory'
|
||||||
47
inventory/migrations/0001_initial.py
Normal file
47
inventory/migrations/0001_initial.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-06 04:31
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
inventory/migrations/__init__.py
Normal file
0
inventory/migrations/__init__.py
Normal file
BIN
inventory/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
inventory/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
inventory/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
inventory/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
59
inventory/models.py
Normal file
59
inventory/models.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from django.db import models
|
||||||
|
from datetime import timedelta, date
|
||||||
|
|
||||||
|
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)
|
||||||
34
inventory/serializers.py
Normal file
34
inventory/serializers.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import Nomenclature, Sticker, StickerMovement
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
|
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__'
|
||||||
3
inventory/tests.py
Normal file
3
inventory/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
13
inventory/urls.py
Normal file
13
inventory/urls.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import NomenclatureViewSet, StickerViewSet, StickerMovementViewSet, ExternalNomenclatureView
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'nomenclature', NomenclatureViewSet)
|
||||||
|
router.register(r'stickers', StickerViewSet)
|
||||||
|
router.register(r'movement', StickerMovementViewSet)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('api/', include(router.urls)),
|
||||||
|
path('api/external-nomenclature/', ExternalNomenclatureView.as_view(), name="external-nomenclature"),
|
||||||
|
]
|
||||||
122
inventory/views.py
Normal file
122
inventory/views.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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
|
||||||
22
manage.py
Normal file
22
manage.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'barcode.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
0
scan/__init__.py
Normal file
0
scan/__init__.py
Normal file
BIN
scan/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
scan/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scan/__pycache__/admin.cpython-312.pyc
Normal file
BIN
scan/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scan/__pycache__/apps.cpython-312.pyc
Normal file
BIN
scan/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scan/__pycache__/models.cpython-312.pyc
Normal file
BIN
scan/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scan/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
scan/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scan/__pycache__/urls.cpython-312.pyc
Normal file
BIN
scan/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scan/__pycache__/views.cpython-312.pyc
Normal file
BIN
scan/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
scan/admin.py
Normal file
3
scan/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
53
scan/api_client.py
Normal file
53
scan/api_client.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# api_client.py
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from .constants import API_URLS, CREDENTIALS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class APIClient:
|
||||||
|
@staticmethod
|
||||||
|
def get_token(url):
|
||||||
|
"""Получение токена авторизации"""
|
||||||
|
payload = {
|
||||||
|
"UserName": CREDENTIALS["USERNAME"],
|
||||||
|
"Password": CREDENTIALS["PASSWORD"]
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
return data["Value"]["Token"] if data.get("IsSuccess") else None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Token request error: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_request(url, token, payload=None, timeout=10):
|
||||||
|
"""Выполнение API-запроса"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
json=payload or {},
|
||||||
|
headers=headers,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"API request error: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_response(status, message=None, data=None, status_code=200):
|
||||||
|
"""Формирование стандартного ответа API"""
|
||||||
|
return Response({
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
"data": data
|
||||||
|
}, status=status_code)
|
||||||
6
scan/apps.py
Normal file
6
scan/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ScanConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'scan'
|
||||||
11
scan/celery_app.py
Normal file
11
scan/celery_app.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
# Указываем путь к настройкам Django
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "barcode.settings")
|
||||||
|
|
||||||
|
app = Celery("barcode")
|
||||||
|
|
||||||
|
# Загружаем настройки Celery из Django
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
app.autodiscover_tasks()
|
||||||
19
scan/constants.py
Normal file
19
scan/constants.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# constants.py
|
||||||
|
API_URLS = {
|
||||||
|
"LOGIN": "http://192.168.254.2:8280/api/login",
|
||||||
|
"LOGIN_ASPU": "http://192.168.254.10:3428/api/login",
|
||||||
|
"STORAGE_INFO": "http://192.168.254.2:8180/api/Storage/getInfo",
|
||||||
|
"ASPU_CODE_INFO": "http://192.168.254.10:3428/api/Codes/getInfo",
|
||||||
|
"MANAGEMENT_ENDPOINTS": {
|
||||||
|
"Sipa": "http://192.168.254.10:3428/api/Management/",
|
||||||
|
"Devin": "http://192.168.254.20:3428/api/Management/",
|
||||||
|
"JR": "http://192.168.254.30:3428/api/Management/",
|
||||||
|
"5L": "http://192.168.254.40:3428/api/Management/",
|
||||||
|
"19L": "http://192.168.254.50:3428/api/Management/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CREDENTIALS = {
|
||||||
|
"USERNAME": "user",
|
||||||
|
"PASSWORD": "user"
|
||||||
|
}
|
||||||
32
scan/migrations/0001_initial.py
Normal file
32
scan/migrations/0001_initial.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-06 04:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MessagesAlert',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type_message', models.CharField(max_length=255, unique=True)),
|
||||||
|
('product_line', models.CharField(max_length=255, unique=True)),
|
||||||
|
('code_lexer', models.IntegerField()),
|
||||||
|
('time', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ScannedCode',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=255, unique=True)),
|
||||||
|
('scanned_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
scan/migrations/__init__.py
Normal file
0
scan/migrations/__init__.py
Normal file
BIN
scan/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
scan/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scan/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
scan/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
15
scan/models.py
Normal file
15
scan/models.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class ScannedCode(models.Model):
|
||||||
|
code = models.CharField(max_length=255, unique=True)
|
||||||
|
scanned_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
class MessagesAlert(models.Model):
|
||||||
|
type_message = models.CharField(max_length=255, unique=True)
|
||||||
|
product_line = models.CharField(max_length=255, unique=True)
|
||||||
|
code_lexer = models.IntegerField()
|
||||||
|
time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
7
scan/serializers.py
Normal file
7
scan/serializers.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import ScannedCode
|
||||||
|
|
||||||
|
class ScannedCodeSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ScannedCode
|
||||||
|
fields = "__all__"
|
||||||
13
scan/tasks.py
Normal file
13
scan/tasks.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
import requests
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def fetch_api_data(url, token):
|
||||||
|
"""Запрос к API в фоновом режиме"""
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, timeout=10)
|
||||||
|
if response.ok:
|
||||||
|
return response.json()
|
||||||
|
except requests.RequestException:
|
||||||
|
return None
|
||||||
3
scan/tests.py
Normal file
3
scan/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
10
scan/urls.py
Normal file
10
scan/urls.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
# from .views import ScanAPIView, ScanAspuAPIView, GetManagementAPIView
|
||||||
|
from .views import GetManagementAPIView
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# path('api/scan/', ScanAPIView.as_view(), name='scan'),
|
||||||
|
# path('api/scan_aspu/', ScanAspuAPIView.as_view(), name='scan_aspu'),
|
||||||
|
path('api/get_management/', GetManagementAPIView.as_view(), name='get_management'),
|
||||||
|
]
|
||||||
83
scan/utils.py
Normal file
83
scan/utils.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from .constants import *
|
||||||
|
|
||||||
|
def get_new_token(url):
|
||||||
|
|
||||||
|
"""Function to get a new token."""
|
||||||
|
|
||||||
|
payload = {"UserName": USERNAME, "Password": PASSWORD}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload)
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and response_data.get("IsSuccess"):
|
||||||
|
|
||||||
|
return response_data["Value"]["Token"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
|
||||||
|
logging.error(f"Network error while getting token: {str(e)}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def request_to_api(url, token, payload=None, timeout=10):
|
||||||
|
|
||||||
|
"""General function to make API requests."""
|
||||||
|
|
||||||
|
if payload is None:
|
||||||
|
|
||||||
|
payload = {}
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
response = requests.post(url, json=payload, headers=headers, timeout=timeout)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
logging.error(f"Error from API ({url}): {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except requests.Timeout:
|
||||||
|
|
||||||
|
logging.error(f"Timeout while requesting {url}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
|
||||||
|
logging.error(f"Request error to {url}: {str(e)}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_response(status, message=None, data=None, status_code=200):
|
||||||
|
|
||||||
|
"""Function to create consistent API responses."""
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
|
||||||
|
"status": status,
|
||||||
|
|
||||||
|
"message": message,
|
||||||
|
|
||||||
|
"data": data
|
||||||
|
|
||||||
|
}, status=status_code)
|
||||||
215
scan/views.py
Normal file
215
scan/views.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from .models import ScannedCode
|
||||||
|
from .serializers import ScannedCodeSerializer
|
||||||
|
|
||||||
|
# Константы API URL и креденшелы
|
||||||
|
LOGIN_URL_ASPU = "http://192.168.254.10:3428/api/login"
|
||||||
|
API_URL_ASPU_MANAGEMENT = "http://192.168.254.10:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_DEVIN = "http://192.168.254.20:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_JR = "http://192.168.254.30:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_5L = "http://192.168.254.40:3428/api/Management/"
|
||||||
|
API_URL_MANAGEMENT_19L = "http://192.168.254.50:3428/api/Management/"
|
||||||
|
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_SIPA = "http://192.168.254.10:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_DEVIN = "http://192.168.254.20:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_JR = "http://192.168.254.30:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_5L = "http://192.168.254.40:3428/api/Messages/"
|
||||||
|
API_URL_ASPU_MASSAGE_LIST_19L = "http://192.168.254.50:3428/api/Messages/"
|
||||||
|
|
||||||
|
USERNAME = "superuser"
|
||||||
|
PASSWORD = "Superuser1105"
|
||||||
|
|
||||||
|
# Глобальные переменные для хранения токена
|
||||||
|
token_cache = {"token": None, "timestamp": 0}
|
||||||
|
TOKEN_LIFETIME = 1440 * 60 # 24 часа в секундах
|
||||||
|
|
||||||
|
|
||||||
|
def get_new_token(url):
|
||||||
|
"""Получение нового токена с кешированием."""
|
||||||
|
global token_cache
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if token_cache["token"] and (current_time - token_cache["timestamp"] < TOKEN_LIFETIME):
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
payload = {"UserName": USERNAME, "Password": PASSWORD}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, timeout=5)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and response_data.get("IsSuccess"):
|
||||||
|
token_cache["token"] = response_data["Value"]["Token"]
|
||||||
|
token_cache["timestamp"] = current_time
|
||||||
|
return token_cache["token"]
|
||||||
|
|
||||||
|
logging.error(f"Ошибка получения токена: {response_data}")
|
||||||
|
return None
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка сети при получении токена: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def request_to_api(url, token, payload=None, timeout=10):
|
||||||
|
"""Отправка запроса к API."""
|
||||||
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload or {}, headers=headers, timeout=timeout)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.Timeout:
|
||||||
|
logging.error(f"Тайм-аут при запросе {url}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Ошибка запроса {url}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_product_id(text):
|
||||||
|
"""Функция извлечения идентификатора продукта из текста."""
|
||||||
|
import re
|
||||||
|
match = re.search(r'Product\s*ID:\s*(\d+)', text)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def create_response(status, message=None, data=None, status_code=200):
|
||||||
|
"""Создание стандартного ответа API."""
|
||||||
|
return Response({
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
"data": data
|
||||||
|
}, status=status_code)
|
||||||
|
|
||||||
|
|
||||||
|
class GetManagementAPIView(APIView):
|
||||||
|
"""Получение данных управления и сообщений с различных источников."""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
token = get_new_token(LOGIN_URL_ASPU)
|
||||||
|
if not token:
|
||||||
|
return create_response("Error", message="Failed to get token", status_code=500)
|
||||||
|
|
||||||
|
management_urls = {
|
||||||
|
"Sipa": API_URL_ASPU_MANAGEMENT,
|
||||||
|
"Devin": API_URL_MANAGEMENT_DEVIN,
|
||||||
|
"JR": API_URL_MANAGEMENT_JR,
|
||||||
|
"5L": API_URL_MANAGEMENT_5L,
|
||||||
|
"19L": API_URL_MANAGEMENT_19L
|
||||||
|
}
|
||||||
|
|
||||||
|
message_urls = {
|
||||||
|
"Sipa": API_URL_ASPU_MASSAGE_LIST_SIPA,
|
||||||
|
"Devin": API_URL_ASPU_MASSAGE_LIST_DEVIN,
|
||||||
|
"JR": API_URL_ASPU_MASSAGE_LIST_JR,
|
||||||
|
"5L": API_URL_ASPU_MASSAGE_LIST_5L,
|
||||||
|
"19L": API_URL_ASPU_MASSAGE_LIST_19L
|
||||||
|
}
|
||||||
|
|
||||||
|
message_payload = {
|
||||||
|
"skip": 1,
|
||||||
|
"take": 50,
|
||||||
|
"includes": [],
|
||||||
|
"sort": [{"field": "Time", "dir": "desc"}],
|
||||||
|
"filter": {"filters": [], "logic": "and"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Используем defaultdict, чтобы избежать KeyError
|
||||||
|
all_data = defaultdict(lambda: {
|
||||||
|
"Name": "",
|
||||||
|
"ShortName": "",
|
||||||
|
"BatchName": "",
|
||||||
|
"Stats": [],
|
||||||
|
"Sources": [],
|
||||||
|
"Gtin": "",
|
||||||
|
"GroupType": "",
|
||||||
|
"Quantity": 0,
|
||||||
|
"Messages": []
|
||||||
|
})
|
||||||
|
|
||||||
|
def process_management_response(key, response_data):
|
||||||
|
"""Обработка данных управления."""
|
||||||
|
if not response_data or "Value" not in response_data:
|
||||||
|
logging.error(f"Некорректный ответ от {key}: {response_data}")
|
||||||
|
return
|
||||||
|
|
||||||
|
value_data = response_data["Value"]
|
||||||
|
batch_name = value_data.get("BatchName", "")
|
||||||
|
|
||||||
|
for product in value_data.get("ProductTypes", []):
|
||||||
|
product_id = product.get("Id")
|
||||||
|
if not product_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
all_data[product_id].update({
|
||||||
|
"Name": product.get("Name", ""),
|
||||||
|
"ShortName": product.get("ShortName", ""),
|
||||||
|
"BatchName": batch_name,
|
||||||
|
"Gtin": product.get("Gtin", ""),
|
||||||
|
"GroupType": product.get("GroupType", ""),
|
||||||
|
})
|
||||||
|
|
||||||
|
all_data[product_id]["Stats"].extend(product.get("Stats", []))
|
||||||
|
all_data[product_id]["Sources"].append(key)
|
||||||
|
|
||||||
|
if product.get("GroupType") == "Pallet":
|
||||||
|
all_data[product_id]["Quantity"] = sum(
|
||||||
|
stat["Value"] for stat in product.get("Stats", []) if stat.get("Type") == "Validated"
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_message_response(key, response_data):
|
||||||
|
"""Обработка сообщений."""
|
||||||
|
if not response_data or "Value" not in response_data:
|
||||||
|
logging.error(f"Некорректный ответ от {key}: {response_data}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for msg in response_data["Value"]:
|
||||||
|
if "Text" not in msg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"Id": msg["Id"],
|
||||||
|
"Time": msg["Time"],
|
||||||
|
"Type": msg["Type"],
|
||||||
|
"Text": msg["Text"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем в общий список сообщений
|
||||||
|
for product_id in all_data:
|
||||||
|
all_data[product_id]["Messages"].append(message)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
|
futures = {
|
||||||
|
executor.submit(request_to_api, url, token, timeout=2): (key, "management")
|
||||||
|
for key, url in management_urls.items()
|
||||||
|
}
|
||||||
|
futures.update({
|
||||||
|
executor.submit(request_to_api, url, token, payload=message_payload, timeout=2): (key, "messages")
|
||||||
|
for key, url in message_urls.items()
|
||||||
|
})
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
key, request_type = futures[future]
|
||||||
|
try:
|
||||||
|
response_data = future.result()
|
||||||
|
|
||||||
|
if request_type == "management":
|
||||||
|
process_management_response(key, response_data)
|
||||||
|
elif request_type == "messages":
|
||||||
|
process_message_response(key, response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Ошибка обработки {key}: {str(e)}")
|
||||||
|
|
||||||
|
logging.info(f"Отправляем ответ с {len(all_data)} объектами управления")
|
||||||
|
|
||||||
|
return create_response("OK", data=dict(all_data)) if all_data else create_response(
|
||||||
|
"Error", message="No data found", status_code=404
|
||||||
|
)
|
||||||
3
scan/viewss1/__init__.py
Normal file
3
scan/viewss1/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# views/__init__.py
|
||||||
|
from .scan_views import StorageScanView, AspuScanView
|
||||||
|
from .management_views import ManagementDataView
|
||||||
88
scan/viewss1/management_views.py
Normal file
88
scan/viewss1/management_views.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# views/management_views.py
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
import logging
|
||||||
|
from ..api_client import APIClient
|
||||||
|
from ..constants import API_URLS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ManagementDataView(APIView):
|
||||||
|
"""Получение управленческих данных"""
|
||||||
|
client = APIClient()
|
||||||
|
response_handler = APIClient.create_response
|
||||||
|
MAX_WORKERS = 5
|
||||||
|
REQUEST_TIMEOUT = 5
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
token = self.client.get_token(API_URLS["LOGIN_ASPU"])
|
||||||
|
if not token:
|
||||||
|
return self.response_handler(
|
||||||
|
"Error",
|
||||||
|
"Failed to get token",
|
||||||
|
status_code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
all_data = {}
|
||||||
|
with ThreadPoolExecutor(max_workers=self.MAX_WORKERS) as executor:
|
||||||
|
futures = {
|
||||||
|
executor.submit(
|
||||||
|
self.client.make_request,
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
timeout=self.REQUEST_TIMEOUT
|
||||||
|
): name
|
||||||
|
for name, url in API_URLS["MANAGEMENT_ENDPOINTS"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
for future in as_completed(futures):
|
||||||
|
name = futures[future]
|
||||||
|
try:
|
||||||
|
self.process_response(
|
||||||
|
name,
|
||||||
|
future.result(),
|
||||||
|
all_data
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing {name}: {str(e)}")
|
||||||
|
|
||||||
|
return self.response_handler(
|
||||||
|
"OK" if all_data else "Error",
|
||||||
|
data=all_data or None,
|
||||||
|
status_code=200 if all_data else 404
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_response(self, source_name, response, data_store):
|
||||||
|
"""Обработка полученных данных"""
|
||||||
|
if not response or not response.get("Value"):
|
||||||
|
return
|
||||||
|
|
||||||
|
value_data = response["Value"]
|
||||||
|
batch_name = value_data.get("BatchName")
|
||||||
|
|
||||||
|
for product in value_data.get("ProductTypes", []):
|
||||||
|
product_id = product.get("Id")
|
||||||
|
if not product_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if product_id not in data_store:
|
||||||
|
data_store[product_id] = {
|
||||||
|
"Name": product.get("Name"),
|
||||||
|
"ShortName": product.get("ShortName"),
|
||||||
|
"BatchName": batch_name,
|
||||||
|
"Stats": [],
|
||||||
|
"Sources": [],
|
||||||
|
"Gtin": product.get("Gtin"),
|
||||||
|
"GroupType": product.get("GroupType", ""),
|
||||||
|
"Quantity": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
data_store[product_id]["Stats"].extend(product.get("Stats", []))
|
||||||
|
data_store[product_id]["Sources"].append(source_name)
|
||||||
|
|
||||||
|
if product.get("GroupType") == "Pallet":
|
||||||
|
data_store[product_id]["Quantity"] = sum(
|
||||||
|
stat["Value"]
|
||||||
|
for stat in product.get("Stats", [])
|
||||||
|
if stat["Type"] == "Validated"
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user