Compare commits
No commits in common. "bx-597-tgbot" and "main" have entirely different histories.
bx-597-tgb
...
main
|
@ -1 +0,0 @@
|
||||||
back/.venv/
|
|
|
@ -1,5 +0,0 @@
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
.venv/
|
|
||||||
.env
|
|
||||||
env/*.env
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.12 (Course_FirstProject) (3)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (Course_FirstProject) (3)" project-jdk-type="Python SDK" />
|
||||||
|
<component name="PyCharmProfessionalAdvertiser">
|
||||||
|
<option name="shown" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/to_inventory.iml" filepath="$PROJECT_DIR$/.idea/to_inventory.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -87,6 +87,3 @@ local_settings.py
|
||||||
|
|
||||||
.env
|
.env
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
|
||||||
.pgpass
|
|
||||||
/*/migrations/0*
|
|
|
@ -1,36 +0,0 @@
|
||||||
FROM ci.svs-tech.pro/library/node:22-bookworm-slim
|
|
||||||
|
|
||||||
# Set environment variables for Python and unbuffered mode
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
|
||||||
|
|
||||||
# Configure Poetry
|
|
||||||
ENV POETRY_VERSION=1.8.3
|
|
||||||
ENV POETRY_HOME=/opt/poetry
|
|
||||||
ENV POETRY_VENV=/opt/poetry-venv
|
|
||||||
ENV POETRY_CACHE_DIR=/opt/.cache
|
|
||||||
|
|
||||||
ENV WORKING_DIR=/app
|
|
||||||
WORKDIR ${WORKING_DIR}
|
|
||||||
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install curl -y
|
|
||||||
RUN apt-get install python3-pip -y
|
|
||||||
RUN apt-get install python3-venv -y
|
|
||||||
|
|
||||||
ENV PATH="${PATH}:${POETRY_VENV}/bin"
|
|
||||||
# Install poetry separated from system interpreter
|
|
||||||
RUN python3 -m venv $POETRY_VENV \
|
|
||||||
&& $POETRY_VENV/bin/pip install -U pip setuptools \
|
|
||||||
&& $POETRY_VENV/bin/pip install poetry==${POETRY_VERSION}
|
|
||||||
|
|
||||||
|
|
||||||
RUN poetry -vvv --version
|
|
||||||
|
|
||||||
COPY pyproject.toml ./
|
|
||||||
RUN poetry install
|
|
||||||
|
|
||||||
|
|
||||||
COPY . ${WORKING_DIR}
|
|
||||||
|
|
||||||
# CMD ["poetry", "run", "/app/.venv/bin/celery"]
|
|
|
@ -1,16 +0,0 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from celery import Celery
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from celery.utils.log import get_task_logger
|
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
|
||||||
|
|
||||||
celery_app = Celery("api")
|
|
||||||
celery_app.config_from_object(f'django.conf:settings', namespace='CELERY')
|
|
||||||
celery_app.autodiscover_tasks()
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
|
@ -0,0 +1,18 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Counter_agent(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f' Counter_agent {self.id}'
|
||||||
|
|
||||||
|
class element(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
external_id = models.IntegerField()
|
||||||
|
type = models.CharField(max_length=100)
|
||||||
|
photo = models.ImageField(upload_to='.')
|
||||||
|
dop_text = models.TextField()
|
||||||
|
Counter_agent = models.ForeignKey(Counter_agent, related_name='element', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f' element {self.id} (Counter_agent {self.Counter_agent.id})'
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
|
@ -11,10 +11,7 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
|
||||||
import dotenv
|
|
||||||
|
|
||||||
dotenv.load_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env"))
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
@ -23,120 +20,83 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = "django-insecure-ruo!wst&sb8(f9)j5u4rda-w!673lj_-c0a%gx_t@)ff*q*2ze"
|
SECRET_KEY = 'django-insecure-ruo!wst&sb8(f9)j5u4rda-w!673lj_-c0a%gx_t@)ff*q*2ze'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
NGROK_TEMP = os.environ.get("NGROK_TEMP")
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = []
|
||||||
"localhost",
|
|
||||||
NGROK_TEMP,
|
|
||||||
"192.168.106.234",
|
|
||||||
"back",
|
|
||||||
"toinv.svs-tech.pro",
|
|
||||||
]
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
|
||||||
"http://localhost",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://localhost:3001",
|
|
||||||
"http://192.168.106.234:3000",
|
|
||||||
"https://toinv.svs-tech.pro",
|
|
||||||
]
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
|
||||||
"http://localhost",
|
|
||||||
"https://toinv.svs-tech.pro",
|
|
||||||
]
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"corsheaders",
|
'django.contrib.admin',
|
||||||
"django_filters",
|
'django.contrib.auth',
|
||||||
"rest_framework",
|
'django.contrib.contenttypes',
|
||||||
"inventory",
|
'django.contrib.sessions',
|
||||||
"tgbot",
|
'django.contrib.messages',
|
||||||
"django.contrib.admin",
|
'django.contrib.staticfiles',
|
||||||
"django.contrib.auth",
|
'inventory.apps.PostsConfig'
|
||||||
"django.contrib.contenttypes",
|
|
||||||
"django.contrib.sessions",
|
|
||||||
"django.contrib.messages",
|
|
||||||
"django.contrib.staticfiles",
|
|
||||||
"tmc",
|
|
||||||
"djangoviz",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
'django.middleware.security.SecurityMiddleware',
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
"django.middleware.common.CommonMiddleware",
|
'django.middleware.common.CommonMiddleware',
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
"django.middleware.common.CommonMiddleware",
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "api.urls"
|
ROOT_URLCONF = 'api.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
"DIRS": [],
|
'DIRS': [],
|
||||||
"APP_DIRS": True,
|
'APP_DIRS': True,
|
||||||
"OPTIONS": {
|
'OPTIONS': {
|
||||||
"context_processors": [
|
'context_processors': [
|
||||||
"django.template.context_processors.debug",
|
'django.template.context_processors.debug',
|
||||||
"django.template.context_processors.request",
|
'django.template.context_processors.request',
|
||||||
"django.contrib.auth.context_processors.auth",
|
'django.contrib.auth.context_processors.auth',
|
||||||
"django.contrib.messages.context_processors.messages",
|
'django.contrib.messages.context_processors.messages',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = "api.wsgi.application"
|
WSGI_APPLICATION = 'api.wsgi.application'
|
||||||
|
|
||||||
# Rest Framework
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
|
|
||||||
"DEFAULT_PAGINATION_CLASS": "tgbot.pagination.CustomPagination",
|
|
||||||
"PAGE_SIZE": 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
'default': {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
"NAME": os.environ.get("DB_NAME"),
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
"USER": os.environ.get("DB_USER"),
|
|
||||||
"PASSWORD": os.environ.get("DB_PASSWORD"),
|
|
||||||
"HOST": os.environ.get("DB_HOST"),
|
|
||||||
"PORT": os.environ.get("DB_PORT"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CELERY_BROKER_URL = f'redis://{os.getenv("REDIS_HOST", "redis")}:6379/0'
|
|
||||||
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -144,9 +104,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
|
@ -156,52 +116,9 @@ USE_TZ = True
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
LOGGING = {
|
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
|
||||||
"formatters": {
|
|
||||||
"verbose": {
|
|
||||||
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
|
||||||
"style": "{",
|
|
||||||
},
|
|
||||||
"large": {
|
|
||||||
"format": "%(asctime)s %(levelname)s %(process)d %(filename)s:%(lineno)d "
|
|
||||||
+ "%(funcName)s %(message)s "
|
|
||||||
},
|
|
||||||
"simple": {
|
|
||||||
"format": "{levelname} {asctime} {module} {message}",
|
|
||||||
"style": "{",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"handlers": {
|
|
||||||
"console": {
|
|
||||||
"level": "INFO",
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
"formatter": "large",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"handlers": ["console"],
|
|
||||||
"level": "INFO",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ODATA_AUTH = os.environ.get("ODATA_AUTH")
|
|
||||||
|
|
||||||
TGBOT = {
|
|
||||||
"TOKEN": os.environ.get("TG_TOKEN"),
|
|
||||||
"BASE_URL": NGROK_TEMP,
|
|
||||||
"WEBHOOK_URL": f"webhook/{os.environ.get('TG_TOKEN')}",
|
|
||||||
}
|
|
||||||
|
|
||||||
SELECTEL = {
|
|
||||||
"access": os.environ.get("AWS_ACCESS"),
|
|
||||||
"secret": os.environ.get("AWS_SECRET"),
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,29 +14,9 @@ Including another URLconf
|
||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import path
|
||||||
from rest_framework import routers
|
|
||||||
|
|
||||||
from inventory import views
|
|
||||||
from tgbot import views as tgbot_views
|
|
||||||
from tmc import views as tmc_views
|
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
|
||||||
router.register(r'api/partner', views.PartnerViewSet)
|
|
||||||
router.register(r'api/element', views.ElementViewSet)
|
|
||||||
router.register(r'api/inventory', views.InventoryItemViewSet)
|
|
||||||
router.register(r'api/tmc/fields', tmc_views.BaseCustomFieldViewSet)
|
|
||||||
router.register(r'api/tmc/ter', tmc_views.TerritoryViewSet)
|
|
||||||
router.register(r'api/tmc/terdeep', tmc_views.TerritoryItemViewSet)
|
|
||||||
router.register(r'api/tmc/items', tmc_views.CustomTableViewSet)
|
|
||||||
router.register(r'api/tgbot', tgbot_views.TgItemViewSet)
|
|
||||||
router.register(r'api/tgbot_items', tgbot_views.TmcFieldViewset)
|
|
||||||
router.register(r'api/stat', tgbot_views.TmcStatViewset, basename='stat')
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
|
||||||
|
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from .models import Partner, Element
|
|
||||||
|
|
||||||
admin.site.register(Partner)
|
|
||||||
admin.site.register(Element)
|
|
|
@ -1,46 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class Partner(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
external_id = models.CharField()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Partner {self.id}"
|
|
||||||
|
|
||||||
|
|
||||||
class Author(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
telegram_id = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
def str(self):
|
|
||||||
return f"Author {self.id} - {self.name}"
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryItem(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
author = models.ForeignKey(Author, on_delete=models.CASCADE, null=True)
|
|
||||||
partner = models.ForeignKey(
|
|
||||||
Partner, related_name="Inventory", on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Element(models.Model):
|
|
||||||
id = models.AutoField(primary_key=True)
|
|
||||||
external_id = models.CharField()
|
|
||||||
element_id = models.CharField(max_length=100)
|
|
||||||
photo = models.ImageField(upload_to=".", null=True)
|
|
||||||
additional_text = models.TextField(null=True, blank=True, default="")
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
inventory = models.ForeignKey(
|
|
||||||
InventoryItem, related_name="Element", on_delete=models.CASCADE
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Element {self.id} (Inventory {self.inventory.id})"
|
|
|
@ -1,56 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
from rest_framework.validators import UniqueValidator
|
|
||||||
|
|
||||||
from .models import Element, InventoryItem, Partner
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("root")
|
|
||||||
|
|
||||||
|
|
||||||
class PartnerSerializer(serializers.ModelSerializer):
|
|
||||||
total_inventory = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Partner
|
|
||||||
fields = ["id", "external_id", "name", "total_inventory"]
|
|
||||||
|
|
||||||
def get_total_inventory(self, instance):
|
|
||||||
return InventoryItem.objects.filter(partner=instance).count()
|
|
||||||
|
|
||||||
|
|
||||||
class InventorySerializer(serializers.ModelSerializer):
|
|
||||||
partner_name = serializers.CharField(source="partner.name", read_only=True)
|
|
||||||
total_elements = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = InventoryItem
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"created_at",
|
|
||||||
"total_elements",
|
|
||||||
"partner",
|
|
||||||
"partner_name",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_total_elements(self, instance):
|
|
||||||
return Element.objects.filter(inventory=instance).count()
|
|
||||||
|
|
||||||
|
|
||||||
class ElementSerializer(serializers.ModelSerializer):
|
|
||||||
inventory_name = serializers.CharField(source="inventory.name", read_only=True)
|
|
||||||
element_id = serializers.CharField(
|
|
||||||
max_length=100,
|
|
||||||
validators=[UniqueValidator(queryset=Element.objects.all())]
|
|
||||||
)
|
|
||||||
class Meta:
|
|
||||||
model = Element
|
|
||||||
fields = [
|
|
||||||
"id",
|
|
||||||
"external_id",
|
|
||||||
"element_id",
|
|
||||||
"photo",
|
|
||||||
"additional_text",
|
|
||||||
"inventory",
|
|
||||||
"inventory_name",
|
|
||||||
]
|
|
|
@ -1,251 +0,0 @@
|
||||||
import requests
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from .models import Element, InventoryItem, Partner
|
|
||||||
from .serializers import ElementSerializer, InventorySerializer, PartnerSerializer
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("root")
|
|
||||||
|
|
||||||
|
|
||||||
class PartnerViewSet(viewsets.ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint that allows partners to be viewed or edited.
|
|
||||||
"""
|
|
||||||
|
|
||||||
queryset = Partner.objects.all()
|
|
||||||
serializer_class = PartnerSerializer
|
|
||||||
|
|
||||||
@action(detail=False, methods=["get"], url_path=r"external")
|
|
||||||
def get_remote_partners(self, request):
|
|
||||||
params = {
|
|
||||||
"$format": "json",
|
|
||||||
"$select": ",".join(["НаименованиеПолное", "Description", "Ref_Key"]),
|
|
||||||
"$filter": "Недействителен eq false",
|
|
||||||
}
|
|
||||||
remote_url = (
|
|
||||||
"https://1c.svs-tech.pro/UNF/odata/standard.odata/Catalog_Контрагенты?"
|
|
||||||
+ "&".join([f"{p}={params[p]}" for p in params])
|
|
||||||
)
|
|
||||||
data = requests.get(remote_url, headers={"Authorization": settings.ODATA_AUTH})
|
|
||||||
try:
|
|
||||||
parsed_data = data.json()
|
|
||||||
return Response(parsed_data["value"])
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@action(detail=False, methods=["get"], url_path=r"external/(?P<cat_id>[^/.]+)")
|
|
||||||
def get_remote_partner_one(self, request, cat_id):
|
|
||||||
params = {
|
|
||||||
"$format": "json",
|
|
||||||
"$select": ",".join(["НаименованиеПолное", "Description", "Ref_Key"]),
|
|
||||||
}
|
|
||||||
remote_url = (
|
|
||||||
f"https://1c.svs-tech.pro/UNF/odata/standard.odata/Catalog_Контрагенты(guid'{cat_id}')?"
|
|
||||||
+ "&".join([f"{p}={params[p]}" for p in params])
|
|
||||||
)
|
|
||||||
logger.info(remote_url)
|
|
||||||
data = requests.get(remote_url, headers={"Authorization": settings.ODATA_AUTH})
|
|
||||||
try:
|
|
||||||
parsed_data = data.json()
|
|
||||||
return Response(parsed_data)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
def get_depth_cat(id):
|
|
||||||
params = {
|
|
||||||
"$format": "json",
|
|
||||||
"$select": ",".join(["Description", "Ref_Key", "Parent_Key"]),
|
|
||||||
"$filter": f"Parent_Key eq guid'{id}'",
|
|
||||||
}
|
|
||||||
remote_url = (
|
|
||||||
"https://1c.svs-tech.pro/UNF/odata/standard.odata/Catalog_КатегорииНоменклатуры?"
|
|
||||||
+ "&".join([f"{p}={params[p]}" for p in params])
|
|
||||||
)
|
|
||||||
data = requests.get(remote_url, headers={"Authorization": settings.ODATA_AUTH})
|
|
||||||
parsed_data = data.json()
|
|
||||||
return parsed_data["value"]
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = InventoryItem.objects.all()
|
|
||||||
serializer_class = InventorySerializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = InventoryItem.objects.all()
|
|
||||||
partner = self.request.query_params.get("partner_id")
|
|
||||||
if partner is not None:
|
|
||||||
queryset = queryset.filter(partner=partner)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def create(self, request, **kwargs):
|
|
||||||
data = request.data
|
|
||||||
|
|
||||||
# check if partner exist
|
|
||||||
if Partner.objects.filter(external_id=data["partner"]).exists():
|
|
||||||
partner_object = Partner.objects.get(external_id=data["partner"])
|
|
||||||
else:
|
|
||||||
partner_object = Partner.objects.create(
|
|
||||||
external_id=data["partner"],
|
|
||||||
name=data["partner_name"],
|
|
||||||
)
|
|
||||||
partner_serializer = PartnerSerializer(partner_object, many=False)
|
|
||||||
|
|
||||||
inventory_object = InventoryItem.objects.create(
|
|
||||||
partner=partner_object,
|
|
||||||
name=f"{data['partner_name']} {datetime.datetime.now()}",
|
|
||||||
)
|
|
||||||
inventory_serializer = InventorySerializer(inventory_object, many=False)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"partner": partner_serializer.data,
|
|
||||||
"inventory": inventory_serializer.data,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ElementViewSet(viewsets.ModelViewSet):
|
|
||||||
"""
|
|
||||||
API endpoint that allows elements to be viewed or edited.
|
|
||||||
"""
|
|
||||||
|
|
||||||
queryset = Element.objects.all()
|
|
||||||
serializer_class = ElementSerializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = Element.objects.all()
|
|
||||||
inventory = self.request.query_params.get("inventory_id")
|
|
||||||
if inventory is not None:
|
|
||||||
queryset = queryset.filter(inventory=inventory)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def create(self, request, **kwargs):
|
|
||||||
data = request.data
|
|
||||||
|
|
||||||
# check if partner exist
|
|
||||||
if Partner.objects.filter(external_id=data["partner"]).exists():
|
|
||||||
partner_object = Partner.objects.get(external_id=data["partner"])
|
|
||||||
else:
|
|
||||||
partner_object = Partner.objects.create(
|
|
||||||
external_id=data["partner"],
|
|
||||||
name=data["partner_name"],
|
|
||||||
)
|
|
||||||
partner_serializer = PartnerSerializer(partner_object, many=False)
|
|
||||||
|
|
||||||
# check if inventory exist
|
|
||||||
if (
|
|
||||||
"inventory"
|
|
||||||
in data
|
|
||||||
# and InventoryItem.objects.filter(id=data["inventory"]).exists()
|
|
||||||
):
|
|
||||||
inventory_object = InventoryItem.objects.get(id=data["inventory"])
|
|
||||||
else:
|
|
||||||
inventory_object = InventoryItem.objects.create(
|
|
||||||
partner=partner_object,
|
|
||||||
name=f"{data['partner_name']} {datetime.datetime.now()}",
|
|
||||||
)
|
|
||||||
inventory_serializer = InventorySerializer(inventory_object, many=False)
|
|
||||||
|
|
||||||
element_data = {
|
|
||||||
"inventory": inventory_object.id,
|
|
||||||
"external_id": data["element"],
|
|
||||||
"element_id": data["element_id"],
|
|
||||||
}
|
|
||||||
if "element_additional_data" in data:
|
|
||||||
element_data["additional_text"] = data["element_additional_data"]
|
|
||||||
element_serializer = ElementSerializer(data=element_data, many=False)
|
|
||||||
if element_serializer.is_valid():
|
|
||||||
element_data["inventory"] = inventory_object
|
|
||||||
Element.objects.create(**element_data)
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"partner": partner_serializer.data,
|
|
||||||
"inventory": inventory_serializer.data,
|
|
||||||
"element": element_serializer.data,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
element_serializer.errors, status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
@action(
|
|
||||||
detail=False,
|
|
||||||
methods=["get"],
|
|
||||||
url_path=r"external_categories",
|
|
||||||
)
|
|
||||||
def get_remote_categories(self, request, cat_id=None):
|
|
||||||
try:
|
|
||||||
categories = get_depth_cat("87e91e07-7e10-11ee-ab5a-a47a2bd811cb")
|
|
||||||
categories.extend(get_depth_cat("20e1e6f6-a575-11ee-ab60-ec3c37e2e642"))
|
|
||||||
categories.extend(get_depth_cat('1f89290b-ffaf-11ed-ab4e-e3e667c628bd'))
|
|
||||||
return Response(categories)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@action(
|
|
||||||
detail=False,
|
|
||||||
methods=["get"],
|
|
||||||
url_path=r"external_categories/(?P<cat_id>[^/.]+)",
|
|
||||||
)
|
|
||||||
def get_remote_categories_child(self, request, cat_id=None):
|
|
||||||
try:
|
|
||||||
categories = get_depth_cat(cat_id)
|
|
||||||
return Response(categories)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@action(detail=False, methods=["get"], url_path=r"external/(?P<cat_id>[^/.]+)")
|
|
||||||
def get_remote_element(self, request, pk=None, cat_id=None):
|
|
||||||
try:
|
|
||||||
params = {
|
|
||||||
"$format": "json",
|
|
||||||
"$select": ",".join(["Description", "Ref_Key", "Parent_Key"]),
|
|
||||||
"$filter": f"КатегорияНоменклатуры_Key eq guid'{cat_id}'",
|
|
||||||
}
|
|
||||||
remote_url = (
|
|
||||||
"https://1c.svs-tech.pro/UNF/odata/standard.odata/Catalog_Номенклатура?"
|
|
||||||
+ "&".join([f"{p}={params[p]}" for p in params])
|
|
||||||
)
|
|
||||||
data = requests.get(
|
|
||||||
remote_url, headers={"Authorization": settings.ODATA_AUTH}
|
|
||||||
)
|
|
||||||
parsed_data = data.json()
|
|
||||||
return Response(parsed_data["value"])
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@action(detail=False, methods=["get"], url_path=r"external/id/(?P<cat_id>[^/.]+)")
|
|
||||||
def get_remote_element_by_id(self, request, pk=None, cat_id=None):
|
|
||||||
try:
|
|
||||||
params = {
|
|
||||||
"$format": "json",
|
|
||||||
"$select": ",".join(["Description", "Ref_Key", "Parent_Key"]),
|
|
||||||
}
|
|
||||||
remote_url = (
|
|
||||||
f"https://1c.svs-tech.pro/UNF/odata/standard.odata/Catalog_Номенклатура(guid'{cat_id}')?"
|
|
||||||
+ "&".join([f"{p}={params[p]}" for p in params])
|
|
||||||
)
|
|
||||||
data = requests.get(
|
|
||||||
remote_url, headers={"Authorization": settings.ODATA_AUTH}
|
|
||||||
)
|
|
||||||
parsed_data = data.json()
|
|
||||||
return Response(parsed_data)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,25 +2,14 @@
|
||||||
name = "back"
|
name = "back"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Ksenia Miqhailova <k.mikhailova@svs-tech.pro>"]
|
authors = ["k.mikhailova@svs-tech.pro"]
|
||||||
readme = "readme.md"
|
readme = "readme.md"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.10"
|
||||||
Django = "^5.0.6"
|
Django = "^5.0.6"
|
||||||
djangorestframework = "^3.15.1"
|
djangorestframework = "^3.15.1"
|
||||||
psycopg2-binary = "^2.9.9"
|
|
||||||
markdown = "^3.6"
|
|
||||||
django-filter = "^24.2"
|
|
||||||
pillow = "^10.3.0"
|
|
||||||
python-dotenv = "^1.0.1"
|
|
||||||
requests = "^2.32.2"
|
|
||||||
django-cors-headers = "^4.3.1"
|
|
||||||
python-telegram-bot = { extras = ["job-queue"], version = "^21.3" }
|
|
||||||
more-itertools = "^10.3.0"
|
|
||||||
djangoviz = "^0.1.1"
|
|
||||||
celery = { extras = ["redis"], version = "^5.4.0" }
|
|
||||||
boto3 = "^1.34.154"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
taskipy = "^1.12.2"
|
taskipy = "^1.12.2"
|
||||||
|
@ -31,5 +20,4 @@ build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.taskipy.tasks]
|
[tool.taskipy.tasks]
|
||||||
migrate = "python manage.py makemigrations && python manage.py migrate"
|
migrate = "python manage.py makemigrations && python manage.py migrate"
|
||||||
server = "python manage.py runserver 0.0.0.0:8000"
|
server = "python manage.py runserver"
|
||||||
celery = "celery worker --loglevel=info"
|
|
|
@ -1,38 +0,0 @@
|
||||||
asgiref==3.8.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
|
|
||||||
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
|
|
||||||
django-filter==24.2 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e \
|
|
||||||
--hash=sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48
|
|
||||||
django==5.0.6 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905 \
|
|
||||||
--hash=sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f
|
|
||||||
djangorestframework==3.15.1 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6 \
|
|
||||||
--hash=sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1
|
|
||||||
markdown==3.6 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f \
|
|
||||||
--hash=sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224
|
|
||||||
psycopg2==2.9.9 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981 \
|
|
||||||
--hash=sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516 \
|
|
||||||
--hash=sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3 \
|
|
||||||
--hash=sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa \
|
|
||||||
--hash=sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a \
|
|
||||||
--hash=sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693 \
|
|
||||||
--hash=sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372 \
|
|
||||||
--hash=sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e \
|
|
||||||
--hash=sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59 \
|
|
||||||
--hash=sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156 \
|
|
||||||
--hash=sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024 \
|
|
||||||
--hash=sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913 \
|
|
||||||
--hash=sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c
|
|
||||||
sqlparse==0.5.0 ; python_version >= "3.10" and python_version < "4.0" \
|
|
||||||
--hash=sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93 \
|
|
||||||
--hash=sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663
|
|
||||||
typing-extensions==4.12.0 ; python_version >= "3.10" and python_version < "3.11" \
|
|
||||||
--hash=sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8 \
|
|
||||||
--hash=sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594
|
|
||||||
tzdata==2024.1 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" \
|
|
||||||
--hash=sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd \
|
|
||||||
--hash=sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252
|
|
|
@ -1,7 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from .models import TgItem, TmcElement, TmcField
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
admin.site.register(TgItem)
|
|
||||||
admin.site.register(TmcElement)
|
|
||||||
admin.site.register(TmcField)
|
|
|
@ -1,20 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
import threading
|
|
||||||
import os
|
|
||||||
|
|
||||||
class TgbotConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "tgbot"
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
from .updater import tg_bot_updater_instance
|
|
||||||
if not tg_bot_updater_instance.is_run and os.environ.get("RUN_MAIN", None) == "true":
|
|
||||||
threading.Thread(
|
|
||||||
target=(tg_bot_updater_instance.run_func),
|
|
||||||
name="tg_updater_thread",
|
|
||||||
daemon=True,
|
|
||||||
).start()
|
|
||||||
tg_bot_updater_instance.is_run = True
|
|
||||||
|
|
||||||
return super().ready()
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import uuid
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from tmc.models import CustomTable, BaseCustomField, TerritoryItem
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("root")
|
|
||||||
|
|
||||||
|
|
||||||
def group_based_upload_to(instance, filename):
|
|
||||||
logger.info(instance)
|
|
||||||
return "files/image/{}/{}/{}".format(
|
|
||||||
type(instance).__name__.lower(), instance.id, filename
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TmcField(models.Model):
|
|
||||||
field = models.ForeignKey(BaseCustomField, models.RESTRICT)
|
|
||||||
text = models.CharField(null=True, blank=True)
|
|
||||||
file_id = models.CharField(null=True, blank=True)
|
|
||||||
image_aws_url = models.CharField(null=True, blank=True)
|
|
||||||
|
|
||||||
|
|
||||||
class TmcElement(models.Model):
|
|
||||||
tmc = models.ForeignKey(CustomTable, models.RESTRICT)
|
|
||||||
name = models.CharField(null=True, blank=True)
|
|
||||||
field = models.ManyToManyField(TmcField)
|
|
||||||
|
|
||||||
|
|
||||||
class TgItem(models.Model):
|
|
||||||
id = models.UUIDField(
|
|
||||||
# auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
default=uuid.uuid4,
|
|
||||||
editable=False,
|
|
||||||
unique=True,
|
|
||||||
)
|
|
||||||
user_id = models.BigIntegerField()
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
location = models.ForeignKey(TerritoryItem, models.RESTRICT)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True, null=True, blank=True)
|
|
||||||
tmc = models.ManyToManyField(TmcElement)
|
|
||||||
is_check = models.BooleanField()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Tg item {self.name}"
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
from rest_framework import pagination
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
class CustomPagination(pagination.PageNumberPagination):
|
|
||||||
page_size_query_param = 'size'
|
|
||||||
def get_paginated_response(self, data):
|
|
||||||
return Response({
|
|
||||||
'count': self.page.paginator.count,
|
|
||||||
'per_page': self.page.paginator.per_page,
|
|
||||||
'results': data
|
|
||||||
})
|
|
|
@ -1,74 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from django.db.models import Count
|
|
||||||
|
|
||||||
from tmc.models import Territory, CustomTable
|
|
||||||
from tmc.serializers import TerritorySerializer
|
|
||||||
from .models import TgItem, TmcElement, TmcField
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("root")
|
|
||||||
|
|
||||||
|
|
||||||
class CustomTableSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = CustomTable
|
|
||||||
fields = ["name"]
|
|
||||||
|
|
||||||
|
|
||||||
class TmcElementSerializer(serializers.Serializer):
|
|
||||||
tmc__name = serializers.CharField()
|
|
||||||
count = serializers.IntegerField()
|
|
||||||
|
|
||||||
|
|
||||||
class TerritorySerializer(serializers.Serializer):
|
|
||||||
id = serializers.IntegerField()
|
|
||||||
name = serializers.CharField()
|
|
||||||
count = serializers.IntegerField(required=False)
|
|
||||||
|
|
||||||
|
|
||||||
class TgItemSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = TgItem
|
|
||||||
fields = "__all__"
|
|
||||||
depth = 3
|
|
||||||
|
|
||||||
|
|
||||||
class TgStatItemSerializer(serializers.Serializer):
|
|
||||||
location = serializers.SerializerMethodField(required=False)
|
|
||||||
inv_count = serializers.IntegerField()
|
|
||||||
tmc = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
def get_location(self, obj):
|
|
||||||
if not obj.get("location__parent"):
|
|
||||||
return None
|
|
||||||
if isinstance(obj.get("location__parent"), list):
|
|
||||||
location_parent_ids = obj.get("location__parent")
|
|
||||||
queryset = Territory.objects.filter(id__in=obj.get("location__parent"))
|
|
||||||
for q in queryset:
|
|
||||||
q.count = location_parent_ids.count(q.id)
|
|
||||||
serializer = TerritorySerializer(queryset, many=True)
|
|
||||||
else:
|
|
||||||
queryset = Territory.objects.get(id=obj.get("location__parent"))
|
|
||||||
serializer = TerritorySerializer(queryset)
|
|
||||||
|
|
||||||
return serializer.data
|
|
||||||
|
|
||||||
def get_tmc(self, obj):
|
|
||||||
queryset = (
|
|
||||||
TmcElement.objects.filter(id__in=obj.get("tmc"))
|
|
||||||
.values("tmc__name")
|
|
||||||
.annotate(count=Count("id"))
|
|
||||||
)
|
|
||||||
serializer = TmcElementSerializer(queryset, many=True)
|
|
||||||
|
|
||||||
return serializer.data
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
depth = 2
|
|
||||||
|
|
||||||
|
|
||||||
class TmcFieldSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = TmcField
|
|
||||||
fields = "__all__"
|
|
|
@ -1,37 +0,0 @@
|
||||||
import os
|
|
||||||
import boto3
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from celery import shared_task
|
|
||||||
from celery.utils.log import get_task_logger
|
|
||||||
|
|
||||||
from api.celery import celery_app
|
|
||||||
from .models import TmcField
|
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
|
||||||
aws_access_key_id = os.environ.get("AWS_ACCESS")
|
|
||||||
aws_secret_access_key = os.environ.get("AWS_SECRET")
|
|
||||||
|
|
||||||
@celery_app.task
|
|
||||||
def upload_file(file_id):
|
|
||||||
if not TmcField.objects.filter(file_id=file_id).exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
obj = TmcField.objects.get(file_id=file_id)
|
|
||||||
logger.info(f"start upload file {file_id}")
|
|
||||||
file_url = requests.get(f"https://api.telegram.org/bot{settings.TGBOT['TOKEN']}/getFile?file_id={file_id}")
|
|
||||||
file_json = file_url.json()
|
|
||||||
response = f"https://api.telegram.org/file/bot{settings.TGBOT['TOKEN']}/{file_json['result']['file_path']}"
|
|
||||||
logger.info(f"get tg url {response}")
|
|
||||||
r = requests.get(response, stream=True)
|
|
||||||
s3 = boto3.client(
|
|
||||||
service_name="s3",
|
|
||||||
endpoint_url="https://s3.ru-1.storage.selcloud.ru",
|
|
||||||
aws_access_key_id=settings.SELECTEL['access'],
|
|
||||||
aws_secret_access_key=settings.SELECTEL['secret'],
|
|
||||||
)
|
|
||||||
s3.upload_fileobj(r.raw, 'inventorization', file_id)
|
|
||||||
obj.image_aws_url = file_id
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
|
@ -1,615 +0,0 @@
|
||||||
import traceback
|
|
||||||
import html
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import more_itertools as mit
|
|
||||||
|
|
||||||
from telegram import (
|
|
||||||
Update,
|
|
||||||
ReplyParameters,
|
|
||||||
ReplyKeyboardMarkup,
|
|
||||||
ReplyKeyboardRemove,
|
|
||||||
KeyboardButton,
|
|
||||||
InlineKeyboardMarkup,
|
|
||||||
InlineKeyboardButton,
|
|
||||||
)
|
|
||||||
from telegram.ext import (
|
|
||||||
ApplicationBuilder,
|
|
||||||
CommandHandler,
|
|
||||||
MessageHandler,
|
|
||||||
CallbackQueryHandler,
|
|
||||||
CallbackContext,
|
|
||||||
ContextTypes,
|
|
||||||
filters,
|
|
||||||
)
|
|
||||||
from telegram.constants import ParseMode
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from .models import TgItem, TmcElement, TmcField
|
|
||||||
from .tasks import upload_file
|
|
||||||
from tmc.models import CustomTable, BaseCustomField, Territory, TerritoryItem
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def chunk(n, l):
|
|
||||||
return [l[i : i + n] for i in range(0, len(l), n)]
|
|
||||||
|
|
||||||
|
|
||||||
class TgBot:
|
|
||||||
_app = None
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.token = settings.TGBOT["TOKEN"]
|
|
||||||
self.baseurl = settings.TGBOT["BASE_URL"]
|
|
||||||
self.webhook = settings.TGBOT["WEBHOOK_URL"]
|
|
||||||
|
|
||||||
if not self.token or not self.baseurl:
|
|
||||||
raise Exception("no token or baseurl")
|
|
||||||
TgBot.app = (
|
|
||||||
ApplicationBuilder().token(settings.TGBOT["TOKEN"]).updater(None).build()
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_webhook(self):
|
|
||||||
await TgBot.app.bot.setWebhook(
|
|
||||||
f"https://{self.baseurl}{reverse('tgitem-send-tg-data')}",
|
|
||||||
drop_pending_updates=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def start_app(self):
|
|
||||||
await TgBot.app.initialize()
|
|
||||||
|
|
||||||
async def admin_action(self, name, queryset):
|
|
||||||
from .updater import tg_bot_updater_instance
|
|
||||||
|
|
||||||
if name == "admin_get_image":
|
|
||||||
item = queryset
|
|
||||||
try:
|
|
||||||
result = await TgBot.app.bot.get_file(item)
|
|
||||||
tg_bot_updater_instance.return_values[item] = result.file_path
|
|
||||||
except Exception as e:
|
|
||||||
tg_bot_updater_instance.return_values[item] = None
|
|
||||||
if name == "admin_get_name":
|
|
||||||
item = queryset
|
|
||||||
try:
|
|
||||||
result = await TgBot.app.bot.get_chat(item)
|
|
||||||
tg_bot_updater_instance.return_values[item] = result.effective_name
|
|
||||||
except Exception as e:
|
|
||||||
tg_bot_updater_instance.return_values[item] = None
|
|
||||||
|
|
||||||
async def set_handlers(self):
|
|
||||||
TgBot.app.add_handler(
|
|
||||||
CommandHandler("start", self.start, filters.ChatType.PRIVATE)
|
|
||||||
)
|
|
||||||
|
|
||||||
TgBot.app.add_handler(CommandHandler("my", self.my, filters.ChatType.PRIVATE))
|
|
||||||
TgBot.app.add_handler(CommandHandler("inv", self.inv, filters.ChatType.PRIVATE))
|
|
||||||
TgBot.app.add_handler(
|
|
||||||
MessageHandler(
|
|
||||||
(
|
|
||||||
filters.ChatType.PRIVATE
|
|
||||||
& ~filters.COMMAND
|
|
||||||
& (filters.TEXT | filters.PHOTO)
|
|
||||||
),
|
|
||||||
self.inv,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
TgBot.app.add_handler(CommandHandler("ter", self.ter, filters.ChatType.PRIVATE))
|
|
||||||
TgBot.app.add_handler(CallbackQueryHandler(self.ter_back, "ter_back"))
|
|
||||||
TgBot.app.add_handler(CallbackQueryHandler(self.ter_next, "ter_next"))
|
|
||||||
|
|
||||||
TgBot.app.add_handler(
|
|
||||||
MessageHandler(
|
|
||||||
filters.ChatType.PRIVATE & filters.Regex(r"/ter_(\d.*)"),
|
|
||||||
self.terdeep,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
TgBot.app.add_handler(CallbackQueryHandler(self.terdeep_back, "terdeep_back"))
|
|
||||||
TgBot.app.add_handler(CallbackQueryHandler(self.terdeep_next, "terdeep_next"))
|
|
||||||
|
|
||||||
TgBot.app.add_handler(
|
|
||||||
MessageHandler(
|
|
||||||
filters.ChatType.PRIVATE & filters.Regex(r"/terdeep_(\d.*)"),
|
|
||||||
self.terdeep_set,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
TgBot.app.add_handler(CallbackQueryHandler(self.stop_inv, "stop_inv"))
|
|
||||||
TgBot.app.add_handler(CallbackQueryHandler(self.get_inv, r"get_inv@(.*?)"))
|
|
||||||
TgBot.app.add_handler(CallbackQueryHandler(self.add_tmc, r"add_tmc@(.*?)"))
|
|
||||||
TgBot.app.add_handler(
|
|
||||||
CallbackQueryHandler(self.add_element, r"add_element@(.*?)")
|
|
||||||
)
|
|
||||||
TgBot.app.add_error_handler(self.error_handler)
|
|
||||||
|
|
||||||
async def start(self, update: Update, context: CallbackContext):
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
(
|
|
||||||
"Это бот для проведения инвентаризации\n"
|
|
||||||
"<strong>/ter</strong> — список территорий\n"
|
|
||||||
"<strong>/inv</strong> — начать новую инвентаризацию\n"
|
|
||||||
"<strong>/my</strong> — продолжить инвентаризацию\n"
|
|
||||||
),
|
|
||||||
# reply_markup=ForceReply(selective=True),
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def my(self, update: Update, context: CallbackContext):
|
|
||||||
user = update.effective_user
|
|
||||||
current_step = context.chat_data.get("step", None)
|
|
||||||
logger.info(f"Step {current_step} from user {user.full_name} with data {context.chat_data}")
|
|
||||||
|
|
||||||
inv = []
|
|
||||||
async for e in TgItem.objects.filter(user_id=user.id, is_check=False).order_by('-created_at'):
|
|
||||||
inv.append({"name": e.name, "id": str(e.id)})
|
|
||||||
keys = chunk(1, inv)
|
|
||||||
if len(inv) > 0:
|
|
||||||
await update.message.reply_html(
|
|
||||||
("<strong>Ваши инвентаризации</strong>"),
|
|
||||||
reply_markup=InlineKeyboardMarkup(
|
|
||||||
[
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(
|
|
||||||
i["name"],
|
|
||||||
callback_data=f'get_inv@{i["id"]}',
|
|
||||||
)
|
|
||||||
for i in arr
|
|
||||||
]
|
|
||||||
for arr in keys
|
|
||||||
]
|
|
||||||
),
|
|
||||||
reply_parameters=ReplyParameters(message_id=update.message.message_id),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await update.message.reply_html(
|
|
||||||
"У вас нет доступных для редактирования инвентаризаций"
|
|
||||||
)
|
|
||||||
|
|
||||||
def format_username(self, user):
|
|
||||||
return f"Специалист {user.name or user.full_name}, ID <code>{user.id}</code>"
|
|
||||||
|
|
||||||
def format_inv(self, inv):
|
|
||||||
return f"Инвентаризация <strong>{inv.name}</strong> от <code>{inv.created_at.strftime('%x')}</code>"
|
|
||||||
|
|
||||||
async def get_tmc_count(self, inv):
|
|
||||||
tmc_count = []
|
|
||||||
async for e in inv.tmc.values("tmc__name").annotate(
|
|
||||||
count=models.Count("tmc__name")
|
|
||||||
):
|
|
||||||
tmc_count.append(e)
|
|
||||||
return tmc_count
|
|
||||||
|
|
||||||
def format_tmc_count(self, tmc_count):
|
|
||||||
res = ["Всего ТМЦ:"]
|
|
||||||
res.append(", ".join([f"{e['tmc__name']} ({e['count']})" for e in tmc_count]))
|
|
||||||
return " ".join(res)
|
|
||||||
|
|
||||||
def format_tmc_name(self, tmc):
|
|
||||||
return f"Название ТМЦ <code>{tmc.name}</code>"
|
|
||||||
|
|
||||||
def format_element(self, field):
|
|
||||||
return f"Элемент <code>{field.name}</code>"
|
|
||||||
|
|
||||||
def stop_inv_button(self):
|
|
||||||
return [
|
|
||||||
InlineKeyboardButton(
|
|
||||||
"❌❌❌ Остановить инвентаризацию", callback_data="stop_inv"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
async def stop_inv(self, update: Update, context: CallbackContext):
|
|
||||||
query = update.callback_query
|
|
||||||
await update.effective_message.edit_reply_markup(InlineKeyboardMarkup([]))
|
|
||||||
await query.answer()
|
|
||||||
context.chat_data.clear()
|
|
||||||
|
|
||||||
await self.start(update, context)
|
|
||||||
|
|
||||||
async def get_inv(self, update: Update, context: CallbackContext):
|
|
||||||
query = update.callback_query
|
|
||||||
await update.effective_message.edit_reply_markup(InlineKeyboardMarkup([]))
|
|
||||||
await query.answer()
|
|
||||||
|
|
||||||
inv_id = query.data.split("@")[-1]
|
|
||||||
inv = await TgItem.objects.aget(id=inv_id)
|
|
||||||
|
|
||||||
context.chat_data["terdeep_value"] = inv.location_id
|
|
||||||
context.chat_data["inv"] = inv.id
|
|
||||||
context.chat_data["step"] = "name"
|
|
||||||
|
|
||||||
await self.inv(update, context)
|
|
||||||
|
|
||||||
async def add_tmc(self, update: Update, context: CallbackContext):
|
|
||||||
query = update.callback_query
|
|
||||||
if update.callback_query:
|
|
||||||
await update.effective_message.edit_reply_markup(InlineKeyboardMarkup([]))
|
|
||||||
await query.answer()
|
|
||||||
|
|
||||||
tmc_id = query.data.split("@")[-1]
|
|
||||||
tmc = await CustomTable.objects.aget(id=tmc_id)
|
|
||||||
tmc_element = await TmcElement.objects.acreate(tmc=tmc)
|
|
||||||
inv = await TgItem.objects.aget(id=context.chat_data["inv"])
|
|
||||||
|
|
||||||
async for f in tmc.fields.all():
|
|
||||||
tmc_field = await TmcField.objects.acreate(field=f)
|
|
||||||
await tmc_element.field.aadd(tmc_field)
|
|
||||||
|
|
||||||
await inv.tmc.aadd(tmc_element)
|
|
||||||
await inv.asave()
|
|
||||||
|
|
||||||
context.chat_data["tmc"] = tmc_element.id
|
|
||||||
context.chat_data["step"] = "add_tmc"
|
|
||||||
|
|
||||||
await self.inv(update, context)
|
|
||||||
|
|
||||||
async def add_element(self, update: Update, context: CallbackContext):
|
|
||||||
query = update.callback_query
|
|
||||||
if update.callback_query:
|
|
||||||
await update.effective_message.edit_reply_markup(InlineKeyboardMarkup([]))
|
|
||||||
await query.answer()
|
|
||||||
|
|
||||||
inv = await TgItem.objects.aget(id=context.chat_data["inv"])
|
|
||||||
tmc = await inv.tmc.aget(id=context.chat_data["tmc"])
|
|
||||||
|
|
||||||
field_id = query.data.split("@")[-1]
|
|
||||||
tmc_field = await tmc.field.aget(id=field_id)
|
|
||||||
|
|
||||||
context.chat_data["element"] = tmc_field.id
|
|
||||||
context.chat_data["step"] = "add_element"
|
|
||||||
|
|
||||||
await self.inv(update, context)
|
|
||||||
|
|
||||||
def set_step(self, update: Update, context: CallbackContext, prefix="ter"):
|
|
||||||
context.chat_data["step"] = f"{prefix}"
|
|
||||||
context.chat_data[f"{prefix}_start"] = 0
|
|
||||||
context.chat_data[f"{prefix}_count"] = 10
|
|
||||||
context.chat_data[f"{prefix}_renew"] = False
|
|
||||||
|
|
||||||
async def step_btn(
|
|
||||||
self, update: Update, context: CallbackContext, prefix="ter", step_type="plus"
|
|
||||||
):
|
|
||||||
query = update.callback_query
|
|
||||||
await query.answer()
|
|
||||||
step_start = context.chat_data[f"{prefix}_start"]
|
|
||||||
step_count = context.chat_data[f"{prefix}_count"]
|
|
||||||
|
|
||||||
context.chat_data[f"{prefix}_renew"] = True
|
|
||||||
|
|
||||||
if step_type == "plus":
|
|
||||||
context.chat_data[f"{prefix}_start"] += step_count
|
|
||||||
elif step_type == "minus":
|
|
||||||
context.chat_data[f"{prefix}_start"] -= step_count
|
|
||||||
|
|
||||||
func = getattr(self, prefix)
|
|
||||||
await func(update, context)
|
|
||||||
|
|
||||||
async def paged_data(
|
|
||||||
self, update: Update, context: CallbackContext, prefix="ter", queryset=[]
|
|
||||||
):
|
|
||||||
user = update.effective_user
|
|
||||||
current_step = context.chat_data.get("step", None)
|
|
||||||
|
|
||||||
logger.info(f"Step {current_step} from user {user.full_name} with data {context.chat_data}")
|
|
||||||
|
|
||||||
locations = []
|
|
||||||
async for e in queryset:
|
|
||||||
locations.append({"name": e.name, "id": e.id})
|
|
||||||
|
|
||||||
step_start = context.chat_data[f"{prefix}_start"]
|
|
||||||
step_step = context.chat_data[f"{prefix}_count"]
|
|
||||||
step_end = step_start + step_step
|
|
||||||
|
|
||||||
text = "\n".join(
|
|
||||||
[
|
|
||||||
self.format_username(user),
|
|
||||||
"Выберите объект:",
|
|
||||||
"\n".join(
|
|
||||||
[
|
|
||||||
f"/{prefix}_{i['id']} {i['name']}"
|
|
||||||
for i in locations[step_start:step_end]
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
keyboard = []
|
|
||||||
if step_start > 0:
|
|
||||||
keyboard.append(
|
|
||||||
InlineKeyboardButton(text="←", callback_data=f"{prefix}_back")
|
|
||||||
)
|
|
||||||
if step_end < len(locations):
|
|
||||||
keyboard.append(
|
|
||||||
InlineKeyboardButton(text="→", callback_data=f"{prefix}_next")
|
|
||||||
)
|
|
||||||
|
|
||||||
keyboard = [keyboard]
|
|
||||||
|
|
||||||
renew = context.chat_data.get(f"{prefix}_renew", None)
|
|
||||||
logger.info(update.message)
|
|
||||||
if renew:
|
|
||||||
await update.effective_message.edit_text(
|
|
||||||
text,
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
|
||||||
parse_mode=ParseMode.HTML,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
text,
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def ter_back(self, update: Update, context: CallbackContext):
|
|
||||||
await self.step_btn(update, context, "ter", "minus")
|
|
||||||
|
|
||||||
async def ter_next(self, update: Update, context: CallbackContext):
|
|
||||||
await self.step_btn(update, context, "ter", "plus")
|
|
||||||
|
|
||||||
async def ter(self, update: Update, context: CallbackContext):
|
|
||||||
if not update.callback_query:
|
|
||||||
self.set_step(update, context, "ter")
|
|
||||||
|
|
||||||
await self.paged_data(update, context, "ter", Territory.objects.all())
|
|
||||||
|
|
||||||
async def terdeep_back(self, update: Update, context: CallbackContext):
|
|
||||||
await self.step_btn(update, context, "terdeep", "minus")
|
|
||||||
|
|
||||||
async def terdeep_next(self, update: Update, context: CallbackContext):
|
|
||||||
await self.step_btn(update, context, "terdeep", "plus")
|
|
||||||
|
|
||||||
async def terdeep(self, update: Update, context: CallbackContext):
|
|
||||||
if not update.callback_query:
|
|
||||||
self.set_step(update, context, "terdeep")
|
|
||||||
|
|
||||||
parent = context.chat_data.get("terdeep", None)
|
|
||||||
if len(re.findall(r"/ter_(\d.*)", update.effective_message.text)):
|
|
||||||
parent = re.findall(r"/ter_(\d.*)", update.effective_message.text)[0]
|
|
||||||
context.chat_data["terdeep"] = parent
|
|
||||||
context.chat_data["terdeep_renew"] = False
|
|
||||||
|
|
||||||
await self.paged_data(
|
|
||||||
update, context, "terdeep", TerritoryItem.objects.filter(parent_id=parent)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def terdeep_set(self, update: Update, context: CallbackContext):
|
|
||||||
context.chat_data["terdeep_value"] = re.findall(
|
|
||||||
r"/terdeep_(\d.*)", update.effective_message.text
|
|
||||||
)[0]
|
|
||||||
text = (
|
|
||||||
"Вы выбрали территорию инвентаризации\n"
|
|
||||||
"Теперь вы можете начать инвентаризацию /inv"
|
|
||||||
)
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
text=text,
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def inv(self, update: Update, context: CallbackContext):
|
|
||||||
user = update.effective_user
|
|
||||||
current_step = context.chat_data.get("step", None)
|
|
||||||
|
|
||||||
logger.info(f"Step {current_step} from user {user.full_name} with data {context.chat_data}")
|
|
||||||
|
|
||||||
if not context.chat_data.get("terdeep_value", None):
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
text=("Вы не выбрали территорию /ter"),
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if update.effective_message.text == "/inv":
|
|
||||||
if 'inv' in context.chat_data:
|
|
||||||
del context.chat_data["inv"]
|
|
||||||
context.chat_data["step"] = "name"
|
|
||||||
current_ter_id = context.chat_data.get("terdeep_value", None)
|
|
||||||
current_ter = await TerritoryItem.objects.aget(id=current_ter_id)
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
"\n".join([self.format_username(user), f"Введите название объекта"]),
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
reply_markup=ReplyKeyboardMarkup([[KeyboardButton(current_ter.name)]]),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if current_step == "name":
|
|
||||||
if not context.chat_data.get("inv", None):
|
|
||||||
current_ter_id = context.chat_data.get("terdeep_value", None)
|
|
||||||
current_ter = await TerritoryItem.objects.aget(id=current_ter_id)
|
|
||||||
inv = await TgItem.objects.acreate(
|
|
||||||
user_id=user.id, location=current_ter
|
|
||||||
)
|
|
||||||
inv.name = update.message.text
|
|
||||||
await inv.asave()
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
f"Ок, сохранено название {inv.name}",
|
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
|
||||||
)
|
|
||||||
# await res.delete()
|
|
||||||
else:
|
|
||||||
inv = await TgItem.objects.aget(id=context.chat_data["inv"])
|
|
||||||
|
|
||||||
tmc = []
|
|
||||||
async for e in CustomTable.objects.all():
|
|
||||||
tmc.append({"name": e.name, "id": e.id})
|
|
||||||
keys = chunk(2, tmc)
|
|
||||||
|
|
||||||
context.chat_data["inv"] = inv.id
|
|
||||||
context.chat_data["step"] = "add_tmc"
|
|
||||||
|
|
||||||
text = "\n".join(
|
|
||||||
[
|
|
||||||
self.format_inv(inv),
|
|
||||||
self.format_tmc_count(await self.get_tmc_count(inv)),
|
|
||||||
"Выберите, какую ТМЦ вы осматриваете:",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
keyboard = [
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(
|
|
||||||
i["name"],
|
|
||||||
callback_data=f'{context.chat_data["step"]}@{i["id"]}',
|
|
||||||
)
|
|
||||||
for i in arr
|
|
||||||
]
|
|
||||||
for arr in keys
|
|
||||||
]
|
|
||||||
keyboard.append(self.stop_inv_button())
|
|
||||||
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
text,
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
reply_markup=InlineKeyboardMarkup(keyboard),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif current_step == "add_tmc":
|
|
||||||
inv = await TgItem.objects.aget(id=context.chat_data["inv"])
|
|
||||||
tmc_element = await inv.tmc.aget(id=context.chat_data["tmc"])
|
|
||||||
tmc = await CustomTable.objects.aget(id=tmc_element.tmc_id)
|
|
||||||
|
|
||||||
fields = []
|
|
||||||
async for e in tmc_element.field.filter(text=None, file_id=None):
|
|
||||||
f = await BaseCustomField.objects.aget(id=e.field_id)
|
|
||||||
fields.append({"name": f.name, "id": e.id})
|
|
||||||
keys = chunk(1, fields)
|
|
||||||
|
|
||||||
context.chat_data["tmc"] = tmc_element.id
|
|
||||||
context.chat_data["step"] = "add_element"
|
|
||||||
|
|
||||||
text = "\n".join(
|
|
||||||
[
|
|
||||||
self.format_inv(inv),
|
|
||||||
self.format_tmc_count(await self.get_tmc_count(inv)),
|
|
||||||
self.format_tmc_name(tmc),
|
|
||||||
f"Что вы загружаете?",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
keyboard = [
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(
|
|
||||||
i["name"],
|
|
||||||
callback_data=f'{context.chat_data["step"]}@{i["id"]}',
|
|
||||||
)
|
|
||||||
for i in arr
|
|
||||||
]
|
|
||||||
for arr in keys
|
|
||||||
]
|
|
||||||
keyboard.append(self.stop_inv_button())
|
|
||||||
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
text,
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
reply_markup=InlineKeyboardMarkup(keyboard),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif current_step == "add_element":
|
|
||||||
inv = await TgItem.objects.aget(id=context.chat_data["inv"])
|
|
||||||
tmc_element = await inv.tmc.aget(id=context.chat_data["tmc"])
|
|
||||||
tmc = await CustomTable.objects.aget(id=tmc_element.tmc_id)
|
|
||||||
element = await TmcField.objects.aget(id=context.chat_data["element"])
|
|
||||||
field = await BaseCustomField.objects.aget(id=element.field_id)
|
|
||||||
|
|
||||||
context.chat_data["step"] = "add_element_data"
|
|
||||||
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
"\n".join(
|
|
||||||
[
|
|
||||||
self.format_inv(inv),
|
|
||||||
self.format_tmc_count(await self.get_tmc_count(inv)),
|
|
||||||
self.format_tmc_name(tmc),
|
|
||||||
self.format_element(field),
|
|
||||||
f"Загрузите фото или пришлите текст",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
reply_parameters=ReplyParameters(
|
|
||||||
message_id=update.effective_message.message_id
|
|
||||||
),
|
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif current_step == "add_element_data":
|
|
||||||
inv = await TgItem.objects.aget(id=context.chat_data["inv"])
|
|
||||||
tmc_element = await inv.tmc.aget(id=context.chat_data["tmc"])
|
|
||||||
tmc = await CustomTable.objects.aget(id=tmc_element.tmc_id)
|
|
||||||
element = await TmcField.objects.aget(id=context.chat_data["element"])
|
|
||||||
field = await BaseCustomField.objects.aget(id=element.field_id)
|
|
||||||
|
|
||||||
if update.message.photo:
|
|
||||||
element.file_id = update.message.photo[-1].file_id
|
|
||||||
upload_file.delay(element.file_id)
|
|
||||||
if update.message.caption:
|
|
||||||
element.text = update.message.caption
|
|
||||||
|
|
||||||
if update.message.text:
|
|
||||||
element.text = update.message.text
|
|
||||||
|
|
||||||
await element.asave()
|
|
||||||
await inv.asave()
|
|
||||||
|
|
||||||
await update.effective_message.reply_html(
|
|
||||||
"Изображение сохранено" if update.message.photo else "Текст сохранен"
|
|
||||||
)
|
|
||||||
|
|
||||||
empty_fields = await tmc_element.field.filter(
|
|
||||||
text=None, file_id=None
|
|
||||||
).acount()
|
|
||||||
if empty_fields > 0:
|
|
||||||
context.chat_data["step"] = "add_tmc"
|
|
||||||
await self.inv(update, context)
|
|
||||||
else:
|
|
||||||
context.chat_data["step"] = "name"
|
|
||||||
await self.inv(update, context)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# logger.info(update.message.entities)
|
|
||||||
logger.info(f"no step for update {update}")
|
|
||||||
|
|
||||||
if "step" in context.chat_data and context.chat_data["step"] == current_step:
|
|
||||||
context.chat_data["step"] = None
|
|
||||||
context.chat_data["inv"] = None
|
|
||||||
|
|
||||||
async def error_handler(
|
|
||||||
self, update: object, context: ContextTypes.DEFAULT_TYPE
|
|
||||||
) -> None:
|
|
||||||
"""Log the error and send a telegram message to notify the developer."""
|
|
||||||
# Log the error before we do anything else, so we can see it even if something breaks.
|
|
||||||
# logger.error("Exception while handling an update:", exc_info=context.error)
|
|
||||||
|
|
||||||
# traceback.format_exception returns the usual python message about an exception, but as a
|
|
||||||
# list of strings rather than a single string, so we have to join them together.
|
|
||||||
tb_list = traceback.format_exception(
|
|
||||||
None, context.error, context.error.__traceback__
|
|
||||||
)
|
|
||||||
tb_string = "".join(tb_list)
|
|
||||||
|
|
||||||
# Build the message with some markup and additional information about what happened.
|
|
||||||
# You might need to add some logic to deal with messages longer than the 4096 character limit.
|
|
||||||
update_str = update.to_dict() if isinstance(update, Update) else str(update)
|
|
||||||
message = (
|
|
||||||
"An exception was raised while handling an update\n"
|
|
||||||
f"<pre>update = {html.escape(json.dumps(update_str, indent=2, ensure_ascii=False))}"
|
|
||||||
"</pre>\n\n"
|
|
||||||
f"<pre>context.chat_data = {html.escape(str(context.chat_data))}</pre>\n\n"
|
|
||||||
f"<pre>context.user_data = {html.escape(str(context.user_data))}</pre>\n\n"
|
|
||||||
f"<pre>{html.escape(tb_string)}</pre>"
|
|
||||||
)
|
|
||||||
# logger.error(context.error)
|
|
||||||
logger.info(f"error in tgbot {context.error}\n{tb_string}\nReply update")
|
|
|
@ -1,71 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import queue
|
|
||||||
from .tgbot import TgBot
|
|
||||||
|
|
||||||
class TgBotUpdater:
|
|
||||||
is_run = False
|
|
||||||
app = None
|
|
||||||
tgbot_class = None
|
|
||||||
my_queue = queue.Queue()
|
|
||||||
|
|
||||||
return_values = dict()
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.tgbot_class = TgBot()
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(e)
|
|
||||||
|
|
||||||
|
|
||||||
async def _set_webhook(self):
|
|
||||||
await self.tgbot_class.set_webhook()
|
|
||||||
|
|
||||||
async def _start_app(self):
|
|
||||||
await self.tgbot_class.start_app()
|
|
||||||
|
|
||||||
async def _set_hadlers(self):
|
|
||||||
await self.tgbot_class.set_handlers()
|
|
||||||
|
|
||||||
async def _run_func(self):
|
|
||||||
from .tgbot import TgBot
|
|
||||||
|
|
||||||
while hasattr(TgBot, "app"):
|
|
||||||
# self.logger.info(f"check updates in {await TgBot.app.bot.get_webhook_info()}")
|
|
||||||
if not self.my_queue.empty():
|
|
||||||
item = self.my_queue.get()
|
|
||||||
if (
|
|
||||||
isinstance(item, dict)
|
|
||||||
and "name" in item
|
|
||||||
and item["name"].startswith("admin_")
|
|
||||||
):
|
|
||||||
await self.tgbot_class.admin_action(item["name"], item["queryset"])
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
await TgBot.app.process_update(item)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error in tg thread {e}")
|
|
||||||
await TgBot.app.process_update(item)
|
|
||||||
self.my_queue.task_done()
|
|
||||||
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
async def main(self):
|
|
||||||
await asyncio.gather(
|
|
||||||
self._set_webhook(),
|
|
||||||
self._set_hadlers(),
|
|
||||||
self._start_app(),
|
|
||||||
self._run_func(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def run_func(self):
|
|
||||||
asyncio.set_event_loop(self.loop)
|
|
||||||
self.loop.run_until_complete(self.main())
|
|
||||||
self.loop.close()
|
|
||||||
|
|
||||||
tg_bot_updater_instance = TgBotUpdater()
|
|
|
@ -1,160 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
|
||||||
from django.http import HttpResponse
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import boto3
|
|
||||||
|
|
||||||
from telegram import Update
|
|
||||||
|
|
||||||
from rest_framework import viewsets, status
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from django.db.models import Count, Subquery, OuterRef, Value
|
|
||||||
from django.db.models.functions import Concat
|
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
|
||||||
|
|
||||||
from .tgbot import TgBot
|
|
||||||
from .updater import tg_bot_updater_instance
|
|
||||||
from .models import TgItem, TmcElement, TmcField
|
|
||||||
from .serializers import TgItemSerializer, TmcFieldSerializer, TgStatItemSerializer
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("root")
|
|
||||||
|
|
||||||
|
|
||||||
class TgItemViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = TgItem.objects.filter(is_check=True).order_by("-created_at")
|
|
||||||
serializer_class = TgItemSerializer
|
|
||||||
http_method_names = ["post", "get", "patch", "delete"]
|
|
||||||
permission_classes = ()
|
|
||||||
authentication_classes = ()
|
|
||||||
|
|
||||||
filter_backends = [DjangoFilterBackend]
|
|
||||||
filterset_fields = ["user_id"]
|
|
||||||
|
|
||||||
def retrieve(self, request, pk=None):
|
|
||||||
item = TgItem.objects.get(id=pk)
|
|
||||||
logger.info(
|
|
||||||
item.tmc.values("tmc__name").annotate(count=models.Count("tmc__name"))
|
|
||||||
)
|
|
||||||
return super().retrieve(request, pk)
|
|
||||||
# return Response(serializer.data)
|
|
||||||
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
|
||||||
if "location_id" in request.data:
|
|
||||||
request.data["location"] = 35
|
|
||||||
return super().partial_update(request)
|
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
if instance.tmc.count() == 0:
|
|
||||||
return super().destroy(request)
|
|
||||||
else:
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
@action(detail=False, methods=["post"], url_path=settings.TGBOT["WEBHOOK_URL"])
|
|
||||||
def send_tg_data(self, request):
|
|
||||||
tg_bot_updater_instance.my_queue.put(
|
|
||||||
Update.de_json(data=json.loads(request.body), bot=TgBot.app.bot)
|
|
||||||
)
|
|
||||||
return Response({"result": "ok"})
|
|
||||||
# return super().create(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class TmcFieldViewset(viewsets.ModelViewSet):
|
|
||||||
queryset = TmcField.objects.all()
|
|
||||||
serializer_class = TmcFieldSerializer
|
|
||||||
http_method_names = ["post", "get", "patch"]
|
|
||||||
permission_classes = ()
|
|
||||||
authentication_classes = ()
|
|
||||||
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
|
||||||
return super().partial_update(request)
|
|
||||||
|
|
||||||
@action(detail=False, methods=["get"], url_path=r"get_name/(?P<chat_id>[^/.]+)")
|
|
||||||
def get_name(self, request, chat_id):
|
|
||||||
tg_bot_updater_instance.my_queue.put(
|
|
||||||
{"name": "admin_get_name", "queryset": chat_id}
|
|
||||||
)
|
|
||||||
response = []
|
|
||||||
timer = 30
|
|
||||||
while timer > 0:
|
|
||||||
sleeping = 1
|
|
||||||
timer -= sleeping
|
|
||||||
time.sleep(sleeping)
|
|
||||||
if chat_id in tg_bot_updater_instance.return_values:
|
|
||||||
response.append(tg_bot_updater_instance.return_values[chat_id])
|
|
||||||
del tg_bot_updater_instance.return_values[chat_id]
|
|
||||||
break
|
|
||||||
return Response(response)
|
|
||||||
|
|
||||||
@action(detail=False, methods=["get"], url_path=r"get_image/(?P<field_id>[^/.]+)")
|
|
||||||
def get_image(self, request, field_id):
|
|
||||||
tg_bot_updater_instance.my_queue.put(
|
|
||||||
{"name": "admin_get_image", "queryset": field_id}
|
|
||||||
)
|
|
||||||
response = []
|
|
||||||
timer = 30
|
|
||||||
while timer > 0:
|
|
||||||
sleeping = 1
|
|
||||||
timer -= sleeping
|
|
||||||
time.sleep(sleeping)
|
|
||||||
if field_id in tg_bot_updater_instance.return_values:
|
|
||||||
response.append(tg_bot_updater_instance.return_values[field_id])
|
|
||||||
del tg_bot_updater_instance.return_values[field_id]
|
|
||||||
break
|
|
||||||
return Response(response)
|
|
||||||
|
|
||||||
@action(detail=False, methods=["get"], url_path=r"get_image_s3/(?P<file_id>[^/.]+)")
|
|
||||||
def get_image_s3(self, request, file_id):
|
|
||||||
s3 = boto3.client(
|
|
||||||
service_name="s3",
|
|
||||||
endpoint_url="https://s3.ru-1.storage.selcloud.ru",
|
|
||||||
aws_access_key_id=settings.SELECTEL["access"],
|
|
||||||
aws_secret_access_key=settings.SELECTEL["secret"],
|
|
||||||
)
|
|
||||||
get_object_response = s3.get_object(Bucket="inventorization", Key=file_id)
|
|
||||||
image = get_object_response["Body"].read()
|
|
||||||
response = HttpResponse(image, content_type="image/jpeg")
|
|
||||||
response["Content-Disposition"] = 'inline; filename="image.jpeg"'
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class TmcStatViewset(viewsets.ViewSet):
|
|
||||||
queryset = TgItem.objects.filter(is_check=True).order_by("-created_at")
|
|
||||||
http_method_names = ["get"]
|
|
||||||
|
|
||||||
def list(self, request):
|
|
||||||
if 'type' in request.query_params and request.query_params['type'] == 'location':
|
|
||||||
queryset = (
|
|
||||||
TgItem.objects.filter(is_check=True)
|
|
||||||
.values("location__parent")
|
|
||||||
.annotate(
|
|
||||||
inv_count=Count("location__parent"),
|
|
||||||
tmc_count=Count("tmc__tmc_id", distinct=True)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
tmc=ArrayAgg("tmc"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
queryset = (
|
|
||||||
TgItem.objects.all()
|
|
||||||
.values("tmc__tmc_id")
|
|
||||||
.annotate(
|
|
||||||
inv_count=Count("location__parent"),
|
|
||||||
tmc_count=Count("id", distinct=True)
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
location__parent=ArrayAgg("location__parent"),
|
|
||||||
tmc=ArrayAgg("tmc"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# logger.info(queryset)
|
|
||||||
# logger.info(TgItem.objects.all().values())
|
|
||||||
serializer = TgStatItemSerializer(queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from .models import BaseCustomField, CustomTable, Territory, TerritoryItem
|
|
||||||
|
|
||||||
admin.site.register(CustomTable)
|
|
||||||
admin.site.register(BaseCustomField)
|
|
||||||
|
|
||||||
admin.site.register(Territory)
|
|
||||||
admin.site.register(TerritoryItem)
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class TmcConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'tmc'
|
|
|
@ -1,19 +0,0 @@
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
import csv
|
|
||||||
|
|
||||||
from tmc.models import Territory, TerritoryItem
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Creating model objects according the file path specified"
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
file_path = "tmc/management/commands/data.csv"
|
|
||||||
with open(file_path, mode="r") as file:
|
|
||||||
csvFile = csv.reader(file)
|
|
||||||
for lines in csvFile:
|
|
||||||
if not Territory.objects.filter(name=lines[0]).exists():
|
|
||||||
parent = Territory.objects.create(name=lines[0])
|
|
||||||
else:
|
|
||||||
parent = Territory.objects.get(name=lines[0])
|
|
||||||
|
|
||||||
if not TerritoryItem.objects.filter(name=lines[1]).exists():
|
|
||||||
TerritoryItem.objects.create(name=lines[1], parent=parent)
|
|
|
@ -1,30 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
class BaseCustomField(models.Model):
|
|
||||||
name = models.CharField(max_length=120, )
|
|
||||||
comment = models.TextField(null=True, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class CustomTable(models.Model):
|
|
||||||
name = models.CharField(max_length=120, )
|
|
||||||
fields = models.ManyToManyField(BaseCustomField)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Territory(models.Model):
|
|
||||||
name = models.CharField()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class TerritoryItem(models.Model):
|
|
||||||
name = models.CharField()
|
|
||||||
parent = models.ForeignKey(Territory, models.RESTRICT)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
|
@ -1,30 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from .models import BaseCustomField, CustomTable, Territory, TerritoryItem
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger("root")
|
|
||||||
|
|
||||||
|
|
||||||
class BaseCustomFieldSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = BaseCustomField
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
|
|
||||||
class CustomTableSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = CustomTable
|
|
||||||
depth = 1
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
class TerritoryItemSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = TerritoryItem
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
class TerritorySerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Territory
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
|
@ -1,36 +0,0 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
from rest_framework import viewsets, filters
|
|
||||||
|
|
||||||
from .models import BaseCustomField, CustomTable, Territory, TerritoryItem
|
|
||||||
from .serializers import (
|
|
||||||
BaseCustomFieldSerializer,
|
|
||||||
CustomTableSerializer,
|
|
||||||
TerritoryItemSerializer,
|
|
||||||
TerritorySerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseCustomFieldViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = BaseCustomField.objects.all()
|
|
||||||
serializer_class = BaseCustomFieldSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class CustomTableViewSet(viewsets.ModelViewSet):
|
|
||||||
page_size_query_param = "size"
|
|
||||||
queryset = CustomTable.objects.all()
|
|
||||||
serializer_class = CustomTableSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class TerritoryItemViewSet(viewsets.ModelViewSet):
|
|
||||||
page_size_query_param = "size"
|
|
||||||
queryset = TerritoryItem.objects.all()
|
|
||||||
serializer_class = TerritoryItemSerializer
|
|
||||||
|
|
||||||
filter_backends = [filters.SearchFilter]
|
|
||||||
search_fields = ["name"]
|
|
||||||
|
|
||||||
|
|
||||||
class TerritoryViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = Territory.objects.all()
|
|
||||||
serializer_class = TerritorySerializer
|
|
|
@ -1,12 +0,0 @@
|
||||||
services:
|
|
||||||
app:
|
|
||||||
entrypoint:
|
|
||||||
- sleep
|
|
||||||
- infinity
|
|
||||||
image: docker/dev-environments-javascript:stable-1
|
|
||||||
init: true
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /var/run/docker.sock
|
|
||||||
target: /var/run/docker.sock
|
|
||||||
|
|
5
dev.sh
5
dev.sh
|
@ -1,4 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
x-terminal-emulator -title "To Invetory FRONT" -e "cd front && npm run dev -p 80 -- --host"&
|
cd front && npm run dev &
|
||||||
x-terminal-emulator -title "To Invetory BACK" -e "cd back && poetry run task server" &&
|
cd back && poetry run task server &
|
||||||
x-terminal-emulator -title "To Invetory BACK" -e "cd back && poetry run task celery"
|
|
|
@ -1,84 +0,0 @@
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
networks:
|
|
||||||
- toinv-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli","ping"]
|
|
||||||
|
|
||||||
back:
|
|
||||||
build:
|
|
||||||
context: ./back
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image: ci.svs-tech.pro/toinv_back:latest
|
|
||||||
command: poetry run python manage.py runserver 0.0.0.0:8000
|
|
||||||
restart: always
|
|
||||||
expose:
|
|
||||||
- "8000"
|
|
||||||
volumes:
|
|
||||||
- ./env/back.env:/app/.env
|
|
||||||
networks:
|
|
||||||
- toinv-network
|
|
||||||
healthcheck:
|
|
||||||
test: curl -f http://localhost:8000/ || exit 1
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 20
|
|
||||||
|
|
||||||
celery:
|
|
||||||
build:
|
|
||||||
context: ./back
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image: ci.svs-tech.pro/toinv_back:latest
|
|
||||||
command: poetry run celery --app api worker --loglevel=info
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./env/back.env:/app/.env
|
|
||||||
networks:
|
|
||||||
- toinv-network
|
|
||||||
depends_on:
|
|
||||||
back:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
front:
|
|
||||||
build:
|
|
||||||
context: ./front
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./env/front.env:/app/.env
|
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
depends_on:
|
|
||||||
back:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- toinv-network
|
|
||||||
image: ci.svs-tech.pro/toinv_front:latest
|
|
||||||
|
|
||||||
nginx:
|
|
||||||
image: ci.svs-tech.pro/nginx:1.25
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "${WEB_PORT:-80}:80"
|
|
||||||
depends_on:
|
|
||||||
back:
|
|
||||||
condition: service_healthy
|
|
||||||
links:
|
|
||||||
- back:back
|
|
||||||
- front:front
|
|
||||||
- redis:redis
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
- ./nginx/conf.d:/etc/nginx/conf.d
|
|
||||||
networks:
|
|
||||||
- toinv-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
toinv-network:
|
|
||||||
driver: bridge
|
|
|
@ -1,12 +0,0 @@
|
||||||
DB_NAME=
|
|
||||||
DB_USER=
|
|
||||||
DB_PASSWORD=
|
|
||||||
DB_HOST=
|
|
||||||
DB_PORT=
|
|
||||||
ODATA_AUTH=
|
|
||||||
|
|
||||||
TG_TOKEN=
|
|
||||||
NGROK_TEMP=
|
|
||||||
|
|
||||||
AWS_ACCESS=
|
|
||||||
AWS_SECRET=
|
|
|
@ -1 +0,0 @@
|
||||||
WEB_PORT=80
|
|
|
@ -1,2 +0,0 @@
|
||||||
NUXT_PUBLIC_API_BASE='http://localhost/api'
|
|
||||||
NUXT_PUBLIC_TGBOT='svstech_inventory_bot'
|
|
|
@ -1,12 +0,0 @@
|
||||||
FROM ci.svs-tech.pro/library/node:22
|
|
||||||
|
|
||||||
RUN mkdir -p /app
|
|
||||||
|
|
||||||
COPY package.json app/package.json
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
RUN npm install --omit=dev
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
RUN npm run build
|
|
||||||
CMD npm run preview -- --host
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default defineAppConfig({
|
|
||||||
ui: {
|
|
||||||
primary: 'pink',
|
|
||||||
gray: 'stone'
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,20 +1,5 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import 'assets/main.scss'
|
|
||||||
import Logo from 'assets/logo.svg'
|
|
||||||
</script>
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div class="header">
|
<NuxtWelcome />
|
||||||
<NuxtLink to="/" class="logo">
|
|
||||||
<Logo />
|
|
||||||
Инвентаризация
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar">
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<NuxtPage />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="m15.5 17.125l4.95-4.95q.275-.275.7-.275t.7.275t.275.7t-.275.7l-5.65 5.65q-.3.3-.7.3t-.7-.3l-2.85-2.85q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v4q0 .425-.288.713T20 10t-.712-.288T19 9V5h-2v2q0 .425-.288.713T16 8H8q-.425 0-.712-.288T7 7V5H5v14h5q.425 0 .713.288T11 20t-.288.713T10 21zm7-16q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"/></svg>
|
|
Before Width: | Height: | Size: 611 B |
|
@ -1,71 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
a[href*='/']:not([class]),
|
|
||||||
a[href*='/'][class=""] {
|
|
||||||
@apply text-primary hover:underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
@apply text-2xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
@apply text-xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
@apply text-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
@apply grid grid-cols-12 mx-auto gap-4
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
@apply col-span-12 flex gap-2 p-2.5 border-b border-primary-700 bg-gradient-to-l from-primary-700;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
@apply text-2xl;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@apply text-3xl inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
@apply col-span-2
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
@apply col-span-10
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
@apply border-b border-primary-200 dark:border-primary-950 py-2.5 bg-gradient-to-l from-primary-200 dark:from-primary-950;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
@apply grid grid-cols-12;
|
|
||||||
|
|
||||||
dt {
|
|
||||||
@apply col-span-full
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
@apply col-span-full col-start-2;
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
@apply mb-2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th.tmc {
|
|
||||||
@apply min-w-[800px];
|
|
||||||
}
|
|
|
@ -1,201 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { apiBase } from '~/helpers';
|
|
||||||
import type { ApiTypeList, ApiTypeExternal, ApiElementSave } from '~/helpers';
|
|
||||||
import type { FormError, FormSubmitEvent } from '#ui/types'
|
|
||||||
|
|
||||||
const props = defineProps(['elements', 'partner'])
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.append("Content-Type", "application/json");
|
|
||||||
|
|
||||||
type ExternalDataType = {
|
|
||||||
partner?: ApiTypeExternal[],
|
|
||||||
categories?: ApiTypeExternal[][],
|
|
||||||
element?: ApiTypeExternal[],
|
|
||||||
}
|
|
||||||
type StateDataType = {
|
|
||||||
partner?: string
|
|
||||||
categories?: string[]
|
|
||||||
inventory?: string
|
|
||||||
element?: string
|
|
||||||
element_id?: string
|
|
||||||
element_additional_data?: string
|
|
||||||
}
|
|
||||||
const loading = ref(false)
|
|
||||||
const state = reactive<StateDataType>({
|
|
||||||
partner: undefined,
|
|
||||||
categories: [],
|
|
||||||
inventory: route.params.inv_id as string || undefined,
|
|
||||||
element: undefined,
|
|
||||||
element_id: undefined,
|
|
||||||
element_additional_data: undefined
|
|
||||||
})
|
|
||||||
const external_data = reactive<ExternalDataType>({
|
|
||||||
partner: [],
|
|
||||||
categories: [],
|
|
||||||
element: [],
|
|
||||||
})
|
|
||||||
const show_error = ref()
|
|
||||||
|
|
||||||
const elements = ref(props.elements)
|
|
||||||
const validate = (state: any): FormError[] => {
|
|
||||||
const errors = []
|
|
||||||
const txt = 'Это поле обязательно'
|
|
||||||
if (!state.partner) errors.push({ path: 'partner', message: txt })
|
|
||||||
if (!external_data || !external_data.partner || !external_data.partner.find(el => el.Ref_Key == state.partner)) {
|
|
||||||
errors.push({ path: 'partner', message: txt })
|
|
||||||
}
|
|
||||||
if (!state.element_id) errors.push({ path: 'element_id', message: txt })
|
|
||||||
// if (!state.element_additional_data) errors.push({ path: 'element_additional_data', message: txt })
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSubmit(event: FormSubmitEvent<any>) {
|
|
||||||
show_error.value = undefined
|
|
||||||
const prepader_data = event.data
|
|
||||||
if (!external_data || !external_data.partner) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
prepader_data.partner_name = external_data.partner.find(el => el.Ref_Key == state.partner)?.Description
|
|
||||||
|
|
||||||
const data = await $fetch<ApiElementSave>(`${apiBase}/element/`, {
|
|
||||||
method: 'POST', body: JSON.stringify(prepader_data), onResponseError: (error) => {
|
|
||||||
if (error.response.status == 500) {
|
|
||||||
show_error.value = Object.entries(error.response._data).map(el => `${el[0]}: ${el[1]}`).join('\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const inv_id = route.params.inv_id || data.inventory?.id
|
|
||||||
if (!route.params.inv_id) {
|
|
||||||
navigateTo(`/organization/p_${data.partner.id}/i_${data.inventory.id}`)
|
|
||||||
} else {
|
|
||||||
const newElements = await $fetch<ApiTypeList>(`${apiBase}/element?inventory_id=${inv_id}`, { headers })
|
|
||||||
elements.value = newElements.results
|
|
||||||
external_data.element = []
|
|
||||||
external_data.categories = []
|
|
||||||
state.categories = []
|
|
||||||
state.element = undefined
|
|
||||||
state.element_id = undefined
|
|
||||||
state.element_additional_data = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchInExternal = (q: string) => {
|
|
||||||
if (!q.length) {
|
|
||||||
return external_data.partner?.splice(0, 10)
|
|
||||||
}
|
|
||||||
return external_data.partner?.filter(el => {
|
|
||||||
return el.Description.toLowerCase().indexOf(q.toLowerCase()) !== -1
|
|
||||||
}).slice(0, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadPartners = async () => {
|
|
||||||
loading.value = true
|
|
||||||
const data = await $fetch<ApiTypeExternal[]>(`${apiBase}/partner/external/`)
|
|
||||||
if (data) {
|
|
||||||
external_data.partner = data
|
|
||||||
if (props.partner) {
|
|
||||||
state.partner = props.partner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
const loadCategories = async () => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
const lastCat = state.categories?.at(-1) || ''
|
|
||||||
const data = await $fetch<ApiTypeExternal[]>(`${apiBase}/element/external_categories/${lastCat}`)
|
|
||||||
if (data.length) {
|
|
||||||
external_data.categories?.push(data)
|
|
||||||
} else {
|
|
||||||
await loadElements()
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
const loadElements = async () => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
const lastCat = state.categories?.at(-1) || ''
|
|
||||||
const data = await $fetch<ApiTypeExternal[]>(`${apiBase}/element/external/${lastCat}`)
|
|
||||||
if (data) {
|
|
||||||
external_data.element = data
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
const loadDeepCategories = async (i: number) => {
|
|
||||||
if ((i + 1) <= (state.categories as string[]).length) {
|
|
||||||
state.categories = state.categories?.slice(0, i + 1)
|
|
||||||
external_data.categories = external_data.categories?.slice(0, i + 1)
|
|
||||||
|
|
||||||
state.element = undefined
|
|
||||||
external_data.element = []
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCategories()
|
|
||||||
}
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadPartners()
|
|
||||||
await loadCategories()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="grid grid-cols-10 gap-4">
|
|
||||||
<div class="col-span-7">
|
|
||||||
<UForm :state="state" :validate="validate" class="flex flex-col gap-4" @submit="onSubmit">
|
|
||||||
<UFormGroup label="Выбрать организацию" name="partner">
|
|
||||||
<USelectMenu v-model="state.partner" :options="external_data.partner" value-attribute="Ref_Key"
|
|
||||||
option-attribute="Description" :searchable="searchInExternal"
|
|
||||||
searchable-placeholder="Выберите организацию из списка контрагентов" :loading="loading"
|
|
||||||
:disabled="!!props.partner" />
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup label="Добавить элемент инвентаризации" v-if="state.partner" :help="!external_data.element?.length ? `Последовательно
|
|
||||||
выбирайте категорию. Если все выбрали, а элементов нет -
|
|
||||||
значит в этой категории нет элементов`: ''">
|
|
||||||
<template v-for="(item, i) in external_data.categories">
|
|
||||||
<USelectMenu v-model="(state.categories as string[])[i]" :options="item"
|
|
||||||
value-attribute="Ref_Key" option-attribute="Description" :searchable="true"
|
|
||||||
:loading="loading" :placeholder="`Категории (${item.length})`"
|
|
||||||
@change="loadDeepCategories(i)" />
|
|
||||||
</template>
|
|
||||||
<USelectMenu v-if="external_data.element?.length" v-model="state.element"
|
|
||||||
:options="external_data.element" value-attribute="Ref_Key" option-attribute="Description"
|
|
||||||
:searchable="true" :loading="loading"
|
|
||||||
:placeholder="`Элементы (${external_data.element.length})`" />
|
|
||||||
</UFormGroup>
|
|
||||||
<div v-if="state.element">
|
|
||||||
<h4>Данные об элементе «{{ external_data.element?.find(el => el['Ref_Key'] ===
|
|
||||||
state.element)?.Description }}»</h4>
|
|
||||||
<UFormGroup label="ID" name="element_id">
|
|
||||||
<UInput placeholder="ID" v-model="state.element_id" />
|
|
||||||
</UFormGroup>
|
|
||||||
<UFormGroup label="Дополнительные сведения" name="element_additional_data">
|
|
||||||
<UTextarea placeholder="Дополнительные сведения" v-model="state.element_additional_data" />
|
|
||||||
</UFormGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UButton type="submit" :disabled="!state.element_id">
|
|
||||||
Сохранить
|
|
||||||
</UButton>
|
|
||||||
</UForm>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-3 flex flex-col gap-4">
|
|
||||||
<h2>{{ external_data.partner?.find(el => el["Ref_Key"] == state.partner)?.Description }}</h2>
|
|
||||||
<UAlert v-if="show_error" title="Server error" :description="show_error" color="primary" />
|
|
||||||
<dl v-if="elements?.length">
|
|
||||||
<template v-for="item in elements">
|
|
||||||
<dt>
|
|
||||||
<Element :id="item.external_id" />
|
|
||||||
</dt>
|
|
||||||
<dd>{{ item.element_id }}</dd>
|
|
||||||
<dd>{{ item.additional_text }}</dd>
|
|
||||||
</template>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style scoped>
|
|
||||||
label {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,32 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { apiBase } from '~/helpers';
|
|
||||||
import type { ApiTypeExternal } from '~/helpers';
|
|
||||||
|
|
||||||
type ExternalNameType = { [s: string]: string; }
|
|
||||||
|
|
||||||
const props = defineProps(['id'])
|
|
||||||
const id = props.id
|
|
||||||
const external_elements = useState<ExternalNameType>('external_elements', () => { return {} })
|
|
||||||
const name = ref()
|
|
||||||
|
|
||||||
const loadOneElement = async (id: string) => {
|
|
||||||
external_elements.value[id] = 'loading'
|
|
||||||
const data = await $fetch<ApiTypeExternal>(`${apiBase}/element/external/id/${id}`)
|
|
||||||
external_elements.value[id] = data.Description
|
|
||||||
name.value = external_elements.value[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (external_elements.value[id]) {
|
|
||||||
name.value = external_elements.value[id]
|
|
||||||
} else {
|
|
||||||
await loadOneElement(id)
|
|
||||||
}
|
|
||||||
watch(external_elements, () => {
|
|
||||||
if (name.value !== external_elements.value[id]) {
|
|
||||||
name.value = external_elements.value[id]
|
|
||||||
}
|
|
||||||
}, { deep: true })
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
{{ name }}
|
|
||||||
</template>
|
|
|
@ -1,21 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useAuthorsStore } from '~/store/authors';
|
|
||||||
|
|
||||||
const props = defineProps(['user_id'])
|
|
||||||
const authorStore = useAuthorsStore()
|
|
||||||
|
|
||||||
const author = ref(authorStore.getItem(props.user_id))
|
|
||||||
const isOpen = ref(false)
|
|
||||||
|
|
||||||
watch(authorStore, () => {
|
|
||||||
author.value = authorStore.getItem(props.user_id)
|
|
||||||
}, { deep: true })
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<template v-if="author.status == 'success'">
|
|
||||||
{{ author.result }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ props.user_id }}
|
|
||||||
</template>
|
|
||||||
</template>
|
|
|
@ -1,42 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useImagesStore } from '~/store/images';
|
|
||||||
|
|
||||||
const props = defineProps(['file_id', 'image_aws_url', 'type'])
|
|
||||||
const imagesStore = useImagesStore()
|
|
||||||
|
|
||||||
const img = ref(imagesStore.getImage(props.file_id, props.image_aws_url ? 'aws' : 'tg'))
|
|
||||||
const isOpen = ref(false)
|
|
||||||
|
|
||||||
const openImage = () => {
|
|
||||||
isOpen.value = !isOpen.value
|
|
||||||
}
|
|
||||||
watch(imagesStore, () => {
|
|
||||||
img.value = imagesStore.getImage(props.file_id, props.image_aws_url ? 'aws' : 'tg')
|
|
||||||
}, { deep: true })
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<template v-if="img.status == 'success'">
|
|
||||||
<template v-if="props.type == 'img'">
|
|
||||||
<a href="#" @click.prevent="openImage">
|
|
||||||
<NuxtImg :src="img.result" />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<a href="#" @click.prevent="openImage">
|
|
||||||
<Icon name="i-mdi-image" />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<UModal v-model="isOpen" :ui="{ width: 'w-full', }">
|
|
||||||
<img :src="img.result" />
|
|
||||||
</UModal>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="img.status == 'error'">
|
|
||||||
<Icon name="i-mdi-close-circle-outline" /> <span class="text-sm text-gray-600">Фото не найдено</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="img.status == 'pending'"> <span class="text-sm text-gray-600">Фото загружается</span>
|
|
||||||
<Icon name="i-mdi-reload" />
|
|
||||||
</template>
|
|
||||||
<template v-else-if="img.status == 'idle'">
|
|
||||||
<Icon name="i-mdi-progress-question" />
|
|
||||||
</template>
|
|
||||||
</template>
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
label: 'Список ТМЦ (шаблоны)',
|
|
||||||
icon: 'i-heroicons-archive-box',
|
|
||||||
to: '/tmc'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Инвентаризации',
|
|
||||||
icon: 'i-heroicons-pencil',
|
|
||||||
to: '/table'
|
|
||||||
}
|
|
||||||
,
|
|
||||||
{
|
|
||||||
label: 'Статистика',
|
|
||||||
icon: 'i-heroicons-funnel',
|
|
||||||
to: '/stat'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<UVerticalNavigation :links="links" />
|
|
||||||
</template>
|
|
|
@ -1,11 +0,0 @@
|
||||||
const config = useRuntimeConfig()
|
|
||||||
export const apiBase = config.public.apiBase
|
|
||||||
|
|
||||||
export const makeColumns = (cols: string[]) => {
|
|
||||||
return cols.map(el => {
|
|
||||||
return {
|
|
||||||
key: el,
|
|
||||||
label: el.toUpperCase()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,17 +1,4 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: false,
|
devtools: { enabled: true }
|
||||||
devtools: { enabled: true },
|
|
||||||
modules: [
|
|
||||||
"@nuxt/ui",
|
|
||||||
"nuxt-svgo",
|
|
||||||
"@pinia/nuxt",
|
|
||||||
"@nuxt/image",
|
|
||||||
],
|
|
||||||
runtimeConfig: {
|
|
||||||
public: {
|
|
||||||
apiBase: '/api',
|
|
||||||
tgbot: ''
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
File diff suppressed because it is too large
Load Diff
|
@ -10,20 +10,8 @@
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/image": "^1.7.0",
|
|
||||||
"@nuxt/ui": "^2.16.0",
|
|
||||||
"@pinia/nuxt": "^0.5.1",
|
|
||||||
"nuxt": "^3.11.2",
|
"nuxt": "^3.11.2",
|
||||||
"nuxt-svgo": "^4.0.1",
|
|
||||||
"pinia": "^2.1.7",
|
|
||||||
"vue": "^3.4.27",
|
"vue": "^3.4.27",
|
||||||
"vue-router": "^4.3.2",
|
"vue-router": "^4.3.2"
|
||||||
"vue3-telegram-login": "^1.1.0",
|
|
||||||
"yup": "^1.4.0",
|
|
||||||
"chart.js": "^3.9.1",
|
|
||||||
"vue-chart-3": "^3.1.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"sass": "^1.77.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
pages index vue
|
|
||||||
</template>
|
|
|
@ -1,54 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const isOrg = ref(true)
|
|
||||||
const data = ref([] as RootObject[])
|
|
||||||
data.value = await apiCall<RootObject[]>(`stat/?type=location`, 'get')
|
|
||||||
|
|
||||||
interface RootObject {
|
|
||||||
location: Location | Location[];
|
|
||||||
inv_count: number;
|
|
||||||
tmc: Tmc[];
|
|
||||||
}
|
|
||||||
interface Tmc {
|
|
||||||
tmc__name: string;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
interface Location {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getData = async () => {
|
|
||||||
isOrg.value = !isOrg.value
|
|
||||||
data.value = []
|
|
||||||
data.value = await apiCall<RootObject[]>(`stat/${isOrg.value == true ? '?type=location' : ''}`, 'get')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="grid grid-cols-12 gap-4">
|
|
||||||
<div class="col-span-12 page-header">
|
|
||||||
<h1>Результаты инвентаризации</h1>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-12">
|
|
||||||
<UButtonGroup>
|
|
||||||
<UButton :disabled="isOrg" @click="getData">По организациям</UButton>
|
|
||||||
<UButton :disabled="!isOrg" @click="getData">По продуктам</UButton>
|
|
||||||
</UButtonGroup>
|
|
||||||
</div>
|
|
||||||
<UCard v-for="item in data" class="col-span-4" v-if="data && isOrg == true">
|
|
||||||
<template #header>
|
|
||||||
{{ item.location.name }} <UBadge>{{ item.inv_count }}</UBadge>
|
|
||||||
</template>
|
|
||||||
<UTable :rows="item.tmc" />
|
|
||||||
</UCard>
|
|
||||||
<UCard v-for="item in data" class="col-span-6" v-if="data && isOrg == false">
|
|
||||||
<template #header>
|
|
||||||
<template v-for="el in item.tmc">
|
|
||||||
{{ el.tmc__name }}
|
|
||||||
</template>
|
|
||||||
<UBadge>{{ item.inv_count }}</UBadge>
|
|
||||||
</template>
|
|
||||||
<UTable :rows="item.location"
|
|
||||||
:columns="[{ key: 'name', label: 'name' }, { key: 'count', label: 'count' }]" />
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,117 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
type location = { id: number, name: string }
|
|
||||||
type tmc_field = { name: string, comment: string, id: number, text: string, file_id: string, image_aws_url: string, }
|
|
||||||
|
|
||||||
type api_field = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
comment: string;
|
|
||||||
}
|
|
||||||
type api_tmc_field = {
|
|
||||||
id: number;
|
|
||||||
text: string;
|
|
||||||
file_id?: any;
|
|
||||||
image_aws_url?: any;
|
|
||||||
field: api_field;
|
|
||||||
}
|
|
||||||
type api_tmc_list = {
|
|
||||||
id: number;
|
|
||||||
name?: any;
|
|
||||||
field: api_tmc_field[];
|
|
||||||
tmc: {
|
|
||||||
id: number,
|
|
||||||
name: string,
|
|
||||||
fields: api_field[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const route = useRoute()
|
|
||||||
const item = ref()
|
|
||||||
const terdeep = ref()
|
|
||||||
const state = reactive({} as
|
|
||||||
{
|
|
||||||
id: string, name?: string,
|
|
||||||
location: location,
|
|
||||||
tmc: {
|
|
||||||
name: string, id: number,
|
|
||||||
fields: tmc_field[]
|
|
||||||
}[]
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
const items_data = await apiCall<any>(`tgbot/${route.params.id}/`)
|
|
||||||
item.value = items_data
|
|
||||||
state.id = items_data.id
|
|
||||||
state.name = items_data.name
|
|
||||||
state.location = items_data.location
|
|
||||||
state.tmc = items_data.tmc.map((el: api_tmc_list) => {
|
|
||||||
return {
|
|
||||||
name: el.tmc.name,
|
|
||||||
id: el.id,
|
|
||||||
fields: el.field.map(item => {
|
|
||||||
return {
|
|
||||||
name: item.field.name,
|
|
||||||
comment: item.field.comment,
|
|
||||||
id: item.id,
|
|
||||||
text: item.text,
|
|
||||||
file_id: item.file_id,
|
|
||||||
image_aws_url: item.image_aws_url,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = async (q: string) => {
|
|
||||||
const terdeep_data = await apiCall<ApiPaged<{ id: number, name: string }>>(`tmc/terdeep/?search=${q}`)
|
|
||||||
terdeep.value = [toRaw(state.location), ...terdeep_data.results]
|
|
||||||
return terdeep.value
|
|
||||||
}
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadData()
|
|
||||||
terdeep.value = [toRaw(state.location)]
|
|
||||||
})
|
|
||||||
const patchField = async (field: { id: number, text: string }) => {
|
|
||||||
await apiCall(`tgbot_items/${field.id}/`, 'patch', { text: field.text })
|
|
||||||
}
|
|
||||||
const patchItem = async () => {
|
|
||||||
await apiCall(`tgbot/${state.id}/`, 'patch', { name: state.name, location_id: state.location })
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<UForm :state="state" v-if="item">
|
|
||||||
<div class="grid grid-cols-4 gap-2 items-end">
|
|
||||||
<div class="col-span-2">
|
|
||||||
<div class="flex items-end gap-2">
|
|
||||||
<UFormGroup label="Название" class="grow">
|
|
||||||
<UInput v-model="state.name" />
|
|
||||||
</UFormGroup>
|
|
||||||
<UButton @click="patchItem">Сохранить</UButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<UFormGroup label="Локация" class="col-span-3">
|
|
||||||
<USelectMenu v-model="state.location.id" :options="terdeep" :searchable="search"
|
|
||||||
value-attribute="id" option-attribute="name" :disabled="true" />
|
|
||||||
</UFormGroup>
|
|
||||||
</div>
|
|
||||||
<UFormGroup label="ТМЦ" v-if="state.tmc" class="col-span-4">
|
|
||||||
<div v-for="el in state.tmc" class="ml-4">
|
|
||||||
<strong>{{ el.name }}</strong>
|
|
||||||
<ul>
|
|
||||||
<li v-for="field in el.fields" class="grid grid-cols-3 gap-4">
|
|
||||||
<span class="col-span-1 flex flex-col gap-2 items-start">
|
|
||||||
{{ field.name }}
|
|
||||||
<UInput v-model="field.text"
|
|
||||||
:placeholder="`Введите ${field.comment || 'с изображения'}`" />
|
|
||||||
<UButton @click="patchField(field)">Сохранить</UButton>
|
|
||||||
</span>
|
|
||||||
<span class="col-span-2 max-w-80">
|
|
||||||
<GetImage :file_id="field.file_id" :image_aws_url="field.image_aws_url" type="img" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</UFormGroup>
|
|
||||||
</div>
|
|
||||||
</UForm>
|
|
||||||
</template>
|
|
|
@ -1,157 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { telegramLoginTemp } from 'vue3-telegram-login'
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const items = ref()
|
|
||||||
const page = ref(1)
|
|
||||||
const pagination = ref({ total: 10, pageCount: 10 })
|
|
||||||
const user_id = ref()
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV == 'development') {
|
|
||||||
user_id.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
loading.value = true
|
|
||||||
const items_data = await apiCall<ApiPaged<TgItem>>(`tgbot/?page=${page.value}&user_id=${user_id.value || ''}`)
|
|
||||||
const res = items_data.results
|
|
||||||
res.map(item => {
|
|
||||||
const uniq = [...new Set(item.tmc.map(el => el.tmc.id))]
|
|
||||||
const uniq_tmc = uniq.map(id => item.tmc.filter(el => el.tmc.id == id))
|
|
||||||
item.uniq = uniq_tmc
|
|
||||||
})
|
|
||||||
items.value = res
|
|
||||||
pagination.value.total = items_data.count
|
|
||||||
pagination.value.pageCount = items_data.per_page
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
onMounted(loadData)
|
|
||||||
watch(page, loadData)
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
key: 'name',
|
|
||||||
label: 'Название'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
class: 'tmc',
|
|
||||||
key: 'tmc',
|
|
||||||
label: 'ТМЦ'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'user_id',
|
|
||||||
label: 'ID автора'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const testCallback = (user: any) => {
|
|
||||||
user_id.value = user.id
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteInv = async (id: string) => {
|
|
||||||
const res = await apiCall<ApiPaged<TgItem>>(`tgbot/${id}/`, 'delete')
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="col-span-12 page-header">
|
|
||||||
<h1>Проведенные инвентаризации</h1>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-12" v-if="user_id == undefined">
|
|
||||||
<UCard>
|
|
||||||
<template #header>
|
|
||||||
Необходимо залогиниться
|
|
||||||
</template>
|
|
||||||
<telegramLoginTemp mode="callback" :telegram-login="config.public.tgbot" @callback="testCallback" />
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-12" v-else>
|
|
||||||
<UTable :rows="items" :columns="columns" :ui="{ td: { base: 'whitespace-normal max-w-sm align-top' } }"
|
|
||||||
:loading="loading">
|
|
||||||
<template #name-data="{ row }">
|
|
||||||
<h3>
|
|
||||||
<NuxtLink :to="`table/${row.id}`">{{ row.name }}</NuxtLink>
|
|
||||||
</h3>
|
|
||||||
Создано: {{ new Date(row.created_at).toLocaleString('ru-RU') }}<br />
|
|
||||||
Обновлено: {{ new Date(row.updated_at).toLocaleString('ru-RU') }}
|
|
||||||
</template>
|
|
||||||
<template #tmc-data="{ row }">
|
|
||||||
<template v-if="row.uniq.length">
|
|
||||||
<div v-for="uniq_tmc in row.uniq" class="inv_element">
|
|
||||||
<h3>{{ uniq_tmc[0].tmc.name }}</h3>
|
|
||||||
<div class="inv_item" v-for="item in uniq_tmc">
|
|
||||||
<ul class="inv_list">
|
|
||||||
<li v-for="el in item.field" class="inv_inner">
|
|
||||||
<span class="inv_inner_name">
|
|
||||||
{{ el.field.name }}:
|
|
||||||
</span>
|
|
||||||
<span class="inv_inner_text" v-if="el.text">
|
|
||||||
{{ el.text }}
|
|
||||||
</span>
|
|
||||||
<UBadge color="gray" v-else>no text</UBadge>
|
|
||||||
<span class="inv_inner_image" v-if="el.file_id">
|
|
||||||
<GetImage :file_id="el.file_id" :image_aws_url="el.image_aws_url" />
|
|
||||||
</span>
|
|
||||||
<UBadge color="gray" v-else>no img</UBadge>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<UButton @click="deleteInv(row.id)" :disabled="loading">Удалить инвентаризацию</UButton>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template #user_id-data="{ row }">
|
|
||||||
<GetAuthor :user_id="row.user_id" />
|
|
||||||
</template>
|
|
||||||
</UTable>
|
|
||||||
<UPagination v-model="page" v-bind="pagination" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
table {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv_element {
|
|
||||||
counter-reset: inv;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv_item {
|
|
||||||
@apply flex align-top gap-1;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
counter-increment: inv;
|
|
||||||
content: counter(inv) "."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv_inner {
|
|
||||||
>* {
|
|
||||||
@apply mr-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&_name {}
|
|
||||||
|
|
||||||
&_text {
|
|
||||||
@apply break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
&_image {}
|
|
||||||
|
|
||||||
&__error {
|
|
||||||
@apply text-red-400 dark:text-red-800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv_list {
|
|
||||||
// @apply flex flex-col gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv_item {
|
|
||||||
@apply mb-2;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,28 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const items = ref()
|
|
||||||
onMounted(async () => {
|
|
||||||
const items_data = await apiCall<ApiPaged<TmcItem>>(`tmc/items/?size=30`)
|
|
||||||
items.value = items_data.results
|
|
||||||
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div class="grid grid-cols-12 gap-4">
|
|
||||||
<div class="col-span-12 page-header">
|
|
||||||
<h1>Список шаблонов ТМЦ</h1>
|
|
||||||
</div>
|
|
||||||
<UCard v-for="item in items" class="col-span-3">
|
|
||||||
<template #header>
|
|
||||||
<div class="prose dark:prose-invert">
|
|
||||||
<h4>{{ item.name }}</h4>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="prose dark:prose-invert text-sm">
|
|
||||||
<p v-for="child in item.fields">
|
|
||||||
<strong>{{ child.name }}</strong><br/>
|
|
||||||
{{ child.comment }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</UCard>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -1,43 +0,0 @@
|
||||||
interface imagesList {
|
|
||||||
[key: string]: {
|
|
||||||
status: 'idle' | 'pending' | 'success' | 'error',
|
|
||||||
result?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const useAuthorsStore = defineStore('authors', {
|
|
||||||
state: () => ({ list: {} as imagesList }),
|
|
||||||
actions: {
|
|
||||||
getItem(name: string) {
|
|
||||||
if (!this.list[name]) {
|
|
||||||
this.list[name] = {
|
|
||||||
status: 'idle',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!Object.entries(this.list).filter(el => el[1].status == 'pending').length &&
|
|
||||||
Object.entries(this.list).filter(el => el[1].status == 'idle').length) {
|
|
||||||
this.loadItems()
|
|
||||||
}
|
|
||||||
return this.list[name]
|
|
||||||
},
|
|
||||||
async loadOneItem(name: string) {
|
|
||||||
const result_data = await apiCall<string[]>(`tgbot_items/get_name/${name}/`)
|
|
||||||
if (result_data.length > 0 && result_data[0] !== null) {
|
|
||||||
this.list[name].status = 'success'
|
|
||||||
this.list[name].result = result_data[0]
|
|
||||||
} else {
|
|
||||||
this.list[name].status = 'error'
|
|
||||||
}
|
|
||||||
if (Object.entries(this.list).filter(el => el[1].status == 'idle').length) {
|
|
||||||
this.loadItems()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadItems() {
|
|
||||||
const elements = Object.entries(this.list).filter(el => el[1].status == 'idle')
|
|
||||||
elements.slice(0, 2).map(el => {
|
|
||||||
this.list[el[0]].status = 'pending'
|
|
||||||
this.loadOneItem(el[0])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,52 +0,0 @@
|
||||||
interface imagesList {
|
|
||||||
[key: string]: {
|
|
||||||
status: 'idle' | 'pending' | 'success' | 'error',
|
|
||||||
type: 'tg' | 'aws',
|
|
||||||
result?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const useImagesStore = defineStore('images', {
|
|
||||||
state: () => ({ list: {} as imagesList }),
|
|
||||||
actions: {
|
|
||||||
getImage(name: string, type: 'tg' | 'aws') {
|
|
||||||
if (!this.list[name]) {
|
|
||||||
this.list[name] = {
|
|
||||||
type,
|
|
||||||
status: 'idle',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!Object.values(this.list).filter(el => el.status == 'pending').length) {
|
|
||||||
this.loadImages()
|
|
||||||
}
|
|
||||||
return this.list[name]
|
|
||||||
},
|
|
||||||
async loadOneImage(name: string) {
|
|
||||||
let file_url_data
|
|
||||||
|
|
||||||
if (this.list[name].type == 'tg') {
|
|
||||||
file_url_data = await apiCall<string[]>(`tgbot_items/get_image/${name}/`)
|
|
||||||
if (file_url_data.length > 0 && file_url_data[0] !== null) {
|
|
||||||
this.list[name].status = 'success'
|
|
||||||
this.list[name].result = file_url_data[0]
|
|
||||||
} else {
|
|
||||||
this.list[name].status = 'error'
|
|
||||||
}
|
|
||||||
} else if (this.list[name].type == 'aws') {
|
|
||||||
file_url_data = await apiCall<string[]>(`tgbot_items/get_image_s3/${name}/`)
|
|
||||||
this.list[name].result = URL.createObjectURL(file_url_data);
|
|
||||||
this.list[name].status = 'success'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.entries(this.list).filter(el => el[1].status == 'idle').length) {
|
|
||||||
this.loadImages()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadImages() {
|
|
||||||
const elements = Object.entries(this.list).filter(el => el[1].status == 'idle')
|
|
||||||
elements.slice(0, 2).map(el => {
|
|
||||||
this.list[el[0]].status = 'pending'
|
|
||||||
this.loadOneImage(el[0])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,64 +0,0 @@
|
||||||
|
|
||||||
declare global {
|
|
||||||
type ApiTypeList = {
|
|
||||||
count: number;
|
|
||||||
next?: any;
|
|
||||||
previous?: any;
|
|
||||||
results: ApiTypeBase[]
|
|
||||||
}
|
|
||||||
type ApiTypeBase =
|
|
||||||
ApiPartner | ApiInventory | ApiElement;
|
|
||||||
|
|
||||||
|
|
||||||
type ApiPartner = { id: number, external_id: number, name: string, total_inventory: number }
|
|
||||||
type ApiInventory = { id: number, partner: number, name: string }
|
|
||||||
type ApiElement = { id: number, external_id: string, element_id: number, photo: string, additional_text: string, inventory: number }
|
|
||||||
type ApiElementSave = {
|
|
||||||
partner: ApiPartner,
|
|
||||||
inventory: ApiInventory,
|
|
||||||
element: ApiElement
|
|
||||||
}
|
|
||||||
type ApiTypeExternal = {
|
|
||||||
'НаименованиеПолное': string;
|
|
||||||
Description: string;
|
|
||||||
Ref_Key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TmcField {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
interface TmcItem {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
fields: TmcField[]
|
|
||||||
}
|
|
||||||
interface TgItem {
|
|
||||||
id: string
|
|
||||||
user_id: number
|
|
||||||
name: string
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
tmc: {
|
|
||||||
id: number,
|
|
||||||
name?: string,
|
|
||||||
tmc: TmcItem,
|
|
||||||
field: {
|
|
||||||
id?: number
|
|
||||||
text?: string
|
|
||||||
file_id?: string
|
|
||||||
fields: TmcField
|
|
||||||
}[]
|
|
||||||
}[]
|
|
||||||
uniq?: any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApiPaged<T> = {
|
|
||||||
count: number;
|
|
||||||
per_page: number
|
|
||||||
next?: any;
|
|
||||||
previous?: any;
|
|
||||||
results: T[]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export { }
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { apiBase } from '~/helpers';
|
|
||||||
|
|
||||||
export default async function <T>(path: string, method = 'GET', body: any = null) {
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.append("Content-Type", "application/json");
|
|
||||||
return await $fetch<T>(`${apiBase}/${path}`, { method, headers, body })
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
user root;
|
|
||||||
worker_processes auto;
|
|
||||||
error_log /var/log/nginx/error.log;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
|
||||||
default upgrade;
|
|
||||||
'' close;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream django {
|
|
||||||
server back:8000;
|
|
||||||
keepalive 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://front:3000;
|
|
||||||
proxy_set_header Host $host:$server_port;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://back:8000/api/;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /admin/ {
|
|
||||||
proxy_pass http://back:8000/admin/;
|
|
||||||
}
|
|
||||||
location /static/admin/ {
|
|
||||||
proxy_pass http://back:8000/static/admin/;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
|
||||||
location = /50x.html {
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
readme.md
11
readme.md
|
@ -1,16 +1,5 @@
|
||||||
# TO INVENTORY
|
# TO INVENTORY
|
||||||
|
|
||||||
## Прод
|
|
||||||
|
|
||||||
* примонтируйте энв файлы
|
|
||||||
* * или запускайте докер с путем к энв `docker compose --env-file ./env/docker.env up`
|
|
||||||
|
|
||||||
## DEV
|
## DEV
|
||||||
|
|
||||||
* Как запустить: `./dev.sh`
|
* Как запустить: `./dev.sh`
|
||||||
|
|
||||||
## Примонтировать энв-файлы
|
|
||||||
|
|
||||||
* `ln -s ~/projects/to_inventory/env/docker.env ~/projects/to_inventory/.env`
|
|
||||||
* `ln -s ~/projects/to_inventory/env/back.env ~/projects/to_inventory/back/.env`
|
|
||||||
* `ln -s ~/projects/to_inventory/env/front.env ~/projects/to_inventory/front/.env`
|
|
Loading…
Reference in New Issue