Compare commits
184 Commits
main
...
bx-597-tgb
Author | SHA1 | Date |
---|---|---|
|
8f1e312f5b | |
|
0cf975820a | |
|
d57429d9c6 | |
|
f63a9ef6b3 | |
|
b45d0029ae | |
|
5404f44fbc | |
|
ecc886689e | |
|
664f081b9f | |
|
6dbac8bd4b | |
|
e6079daf32 | |
|
93b60444ea | |
|
6137d78f3f | |
|
0aafb8c7aa | |
|
64b3271c35 | |
|
6e2cee0968 | |
|
10b1f82bf3 | |
|
463405b955 | |
|
63af4b9891 | |
|
626c8b5b91 | |
|
c7f7675941 | |
|
83d9d85ffd | |
|
1153876c81 | |
|
e67cee0513 | |
|
301e9a0964 | |
|
65de9d7fbd | |
|
666b38f2f9 | |
|
18ecaea6af | |
|
23ef8eab20 | |
|
b1527a8f49 | |
|
a1cbaf86fe | |
|
83602ecb79 | |
|
d9a55e2364 | |
|
3473547171 | |
|
accb2c8c50 | |
|
c74a81c160 | |
|
a59b6ee0a0 | |
|
c92508aa5f | |
|
32adcd2831 | |
|
5e149b21a9 | |
|
59eb963554 | |
|
a415ab31f4 | |
|
5f027e2066 | |
|
f29e667561 | |
|
10842b9a36 | |
|
ebe440af12 | |
|
c97ddc41c7 | |
|
85875778ae | |
|
ad36e16f93 | |
|
c67d171472 | |
|
9593ee02fa | |
|
db671877b1 | |
|
b92de6ac9f | |
|
65f54695d7 | |
|
0809427959 | |
|
f211df9c7d | |
|
8691855301 | |
|
92a1b96e37 | |
|
6a4333e7c1 | |
|
c1aa007b1a | |
|
58994c596c | |
|
c0c6b4a285 | |
|
857244f3fc | |
|
c3fafd5645 | |
|
1550c2fe6e | |
|
679996692a | |
|
e01a416c76 | |
|
449c023dff | |
|
ddfb7994c8 | |
|
ac2040eee9 | |
|
db276f2bec | |
|
5e8f4649a2 | |
|
c36f21b819 | |
|
b231676736 | |
|
29f1391288 | |
|
8e35d2ecb5 | |
|
a38f5ad12c | |
|
24705fe8e0 | |
|
71b914f07f | |
|
1cf0a60c27 | |
|
76b7ce67eb | |
|
424a25ed15 | |
|
5466afc08b | |
|
ce81205317 | |
|
b32f930cbd | |
|
2f725bbfc7 | |
|
b118fb7124 | |
|
463e7451fb | |
|
979c19e67f | |
|
25237f11ed | |
|
2ba26c942b | |
|
b2184eb5c3 | |
|
cbf4c566b0 | |
|
8d52302c7e | |
|
8204f47e72 | |
|
a550d156d2 | |
|
d2064e462c | |
|
71a41e003b | |
|
932d96c203 | |
|
2916826bfb | |
|
1b310dbfbe | |
|
ef08733979 | |
|
431655eaac | |
|
3922bd3454 | |
|
1fea37ec76 | |
|
5b4db31b68 | |
|
2750cea9c4 | |
|
639857b310 | |
|
f89a1f365a | |
|
f1a2a640e1 | |
|
96741cf0a0 | |
|
363fa02663 | |
|
3ef164c72f | |
|
d8d9cef44d | |
|
fdfd8e28f6 | |
|
30f6451727 | |
|
50405639c2 | |
|
b94df630bc | |
|
bafab7f93c | |
|
1fd958a511 | |
|
6a9264ff50 | |
|
9d09431c42 | |
|
ac73ad09ec | |
|
b4ffe1c551 | |
|
b0c632030f | |
|
65b340b873 | |
|
44fb008fd6 | |
|
69fc2eabd8 | |
|
cece90eaa2 | |
|
e8448a91db | |
|
ba0e8ba0d3 | |
|
2a41108ced | |
|
a65fdc02d5 | |
|
c143650bdd | |
|
c4dac25528 | |
|
e6b95be013 | |
|
7bdf4c628d | |
|
28406ad98a | |
|
61d6823e5f | |
|
0c95cda20d | |
|
cf9998c5f9 | |
|
153b7f601e | |
|
a56dcbd4d2 | |
|
877f99f466 | |
|
2e361ff919 | |
|
642b8ca73d | |
|
43668907f0 | |
|
66a298e101 | |
|
15c54bd3e6 | |
|
2dbd6742f7 | |
|
01ba1b8bce | |
|
cb45928ebd | |
|
69672c8721 | |
|
a23e604954 | |
|
516e06b3df | |
|
ef56974447 | |
|
46c3995b6f | |
|
def5f8873f | |
|
e7a91b0014 | |
|
c2edb2fe2a | |
|
659e1ee439 | |
|
fa49de322d | |
|
dd32cf6432 | |
|
43fb670765 | |
|
1dab0e2cc7 | |
|
14c0054d7f | |
|
8c7e4332dd | |
|
e1e353bc51 | |
|
58934e8666 | |
|
b71ebfc398 | |
|
698f791920 | |
|
ebc48e3b87 | |
|
2fad0b67d2 | |
|
2e8cc9f387 | |
|
7b2d7ff294 | |
|
f2f8a62fa2 | |
|
cbfc7daf6f | |
|
2332532236 | |
|
100d78a53b | |
|
2e09d6dcd4 | |
|
7b89dd25e7 | |
|
a21ee1d965 | |
|
a939178e3f | |
|
2c5d3ffe10 | |
|
34db8582e7 |
|
@ -0,0 +1 @@
|
||||||
|
back/.venv/
|
|
@ -0,0 +1,5 @@
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.venv/
|
||||||
|
.env
|
||||||
|
env/*.env
|
|
@ -1,3 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -87,3 +87,6 @@ local_settings.py
|
||||||
|
|
||||||
.env
|
.env
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
|
||||||
|
.pgpass
|
||||||
|
/*/migrations/0*
|
|
@ -0,0 +1,36 @@
|
||||||
|
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"]
|
|
@ -0,0 +1,16 @@
|
||||||
|
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()
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,18 +0,0 @@
|
||||||
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})'
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
|
@ -11,7 +11,10 @@ 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
|
||||||
|
|
||||||
|
@ -20,83 +23,120 @@ 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 = [
|
||||||
'django.contrib.admin',
|
"corsheaders",
|
||||||
'django.contrib.auth',
|
"django_filters",
|
||||||
'django.contrib.contenttypes',
|
"rest_framework",
|
||||||
'django.contrib.sessions',
|
"inventory",
|
||||||
'django.contrib.messages',
|
"tgbot",
|
||||||
'django.contrib.staticfiles',
|
"django.contrib.admin",
|
||||||
'inventory.apps.PostsConfig'
|
"django.contrib.auth",
|
||||||
|
"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",
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
"django.middleware.common.CommonMiddleware",
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"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.sqlite3',
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
"NAME": os.environ.get("DB_NAME"),
|
||||||
|
"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",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -104,9 +144,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
|
||||||
|
|
||||||
|
@ -116,9 +156,52 @@ 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,9 +14,29 @@ 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 path
|
from django.urls import include, 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),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import Partner, Element
|
||||||
|
|
||||||
|
admin.site.register(Partner)
|
||||||
|
admin.site.register(Element)
|
|
@ -0,0 +1,46 @@
|
||||||
|
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})"
|
|
@ -0,0 +1,56 @@
|
||||||
|
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",
|
||||||
|
]
|
|
@ -0,0 +1,251 @@
|
||||||
|
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,14 +2,25 @@
|
||||||
name = "back"
|
name = "back"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["k.mikhailova@svs-tech.pro"]
|
authors = ["Ksenia Miqhailova <k.mikhailova@svs-tech.pro>"]
|
||||||
readme = "readme.md"
|
readme = "readme.md"
|
||||||
package-mode = false
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.11"
|
||||||
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"
|
||||||
|
@ -20,4 +31,5 @@ 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"
|
server = "python manage.py runserver 0.0.0.0:8000"
|
||||||
|
celery = "celery worker --loglevel=info"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
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
|
|
@ -0,0 +1,7 @@
|
||||||
|
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)
|
|
@ -0,0 +1,20 @@
|
||||||
|
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()
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
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}"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
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
|
||||||
|
})
|
|
@ -0,0 +1,74 @@
|
||||||
|
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__"
|
|
@ -0,0 +1,37 @@
|
||||||
|
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
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -0,0 +1,615 @@
|
||||||
|
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")
|
|
@ -0,0 +1,71 @@
|
||||||
|
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()
|
|
@ -0,0 +1,160 @@
|
||||||
|
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)
|
|
@ -0,0 +1,8 @@
|
||||||
|
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)
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TmcConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'tmc'
|
|
@ -0,0 +1,19 @@
|
||||||
|
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)
|
|
@ -0,0 +1,30 @@
|
||||||
|
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
|
|
@ -0,0 +1,30 @@
|
||||||
|
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__"
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
|
@ -0,0 +1,12 @@
|
||||||
|
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,3 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cd front && npm run dev &
|
x-terminal-emulator -title "To Invetory FRONT" -e "cd front && npm run dev -p 80 -- --host"&
|
||||||
cd back && poetry run task server &
|
x-terminal-emulator -title "To Invetory BACK" -e "cd back && poetry run task server" &&
|
||||||
|
x-terminal-emulator -title "To Invetory BACK" -e "cd back && poetry run task celery"
|
|
@ -0,0 +1,84 @@
|
||||||
|
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
|
|
@ -0,0 +1,12 @@
|
||||||
|
DB_NAME=
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_HOST=
|
||||||
|
DB_PORT=
|
||||||
|
ODATA_AUTH=
|
||||||
|
|
||||||
|
TG_TOKEN=
|
||||||
|
NGROK_TEMP=
|
||||||
|
|
||||||
|
AWS_ACCESS=
|
||||||
|
AWS_SECRET=
|
|
@ -0,0 +1 @@
|
||||||
|
WEB_PORT=80
|
|
@ -0,0 +1,2 @@
|
||||||
|
NUXT_PUBLIC_API_BASE='http://localhost/api'
|
||||||
|
NUXT_PUBLIC_TGBOT='svstech_inventory_bot'
|
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
primary: 'pink',
|
||||||
|
gray: 'stone'
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,5 +1,20 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import 'assets/main.scss'
|
||||||
|
import Logo from 'assets/logo.svg'
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="container">
|
||||||
<NuxtWelcome />
|
<div class="header">
|
||||||
|
<NuxtLink to="/" class="logo">
|
||||||
|
<Logo />
|
||||||
|
Инвентаризация
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar">
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<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>
|
After Width: | Height: | Size: 611 B |
|
@ -0,0 +1,71 @@
|
||||||
|
@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];
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
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,4 +1,17 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
devtools: { enabled: true }
|
ssr: false,
|
||||||
|
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,8 +10,20 @@
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
pages index vue
|
||||||
|
</template>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,157 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,43 @@
|
||||||
|
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])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,52 @@
|
||||||
|
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])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,64 @@
|
||||||
|
|
||||||
|
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 { }
|
|
@ -0,0 +1,7 @@
|
||||||
|
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 })
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
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,5 +1,16 @@
|
||||||
# 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