Compare commits
7 Commits
c677e4f8af
...
prod
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fa41e6d46 | |||
| da6d345609 | |||
| 1c14f9bcc1 | |||
| c2ff6d8aad | |||
| f9e5d73868 | |||
| 0413460c3a | |||
| 0520a1117c |
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# .dockerignore
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
test_output/
|
||||||
|
logs/
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.idea/
|
||||||
39
.gitea/workflows/deploy.yml
Normal file
39
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- prod
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_deploy:
|
||||||
|
runs-on: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Define Image Name
|
||||||
|
id: image_vars
|
||||||
|
run: |
|
||||||
|
IMAGE_TAG="${{ GITEA_BRANCH }}-${{ GITEA_SHA::7 }}"
|
||||||
|
FULL_IMAGE_NAME="${{ GITEA_REPO_NAME }}:${IMAGE_TAG}"
|
||||||
|
echo "::set-output name=FULL_IMAGE_NAME::${FULL_IMAGE_NAME}"
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
docker buildx build --load --platform linux/amd64 \
|
||||||
|
-t "${{ steps.image_vars.outputs.FULL_IMAGE_NAME }}" .
|
||||||
|
|
||||||
|
|
||||||
|
- name: Set up SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
|
||||||
|
- name: Save, Transfer and Deploy Image
|
||||||
|
run: |
|
||||||
|
IMAGE_TO_DEPLOY="${{ steps.image_vars.outputs.FULL_IMAGE_NAME }}"
|
||||||
|
echo "Transferring image ${IMAGE_TO_DEPLOY} to production..."
|
||||||
|
docker save "${IMAGE_TO_DEPLOY}" | ssh -o StrictHostKeyChecking=no deployer@YOUR_PROD_SERVER_IP "/home/deployer/deploy.sh '${IMAGE_TO_DEPLOY}'"
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
|||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
*.db
|
*.db
|
||||||
__*
|
__*
|
||||||
|
*.json
|
||||||
|
files/*
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
# Этап 1: Используем официальный легковесный образ Python
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию внутри контейнера
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Обновляем pip и устанавливаем зависимости
|
||||||
|
# Копируем requirements.txt отдельно, чтобы Docker мог кэшировать этот слой.
|
||||||
|
# Зависимости переустановятся только если изменится requirements.txt.
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копируем весь исходный код приложения в рабочую директорию
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Указываем команду, которая будет выполняться при запуске контейнера
|
||||||
|
CMD ["python", "main.py"]
|
||||||
12
config.py
12
config.py
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -12,6 +11,8 @@ if not all([SD_ACCESS_KEY, SD_BASE_URL]):
|
|||||||
raise ValueError("Необходимо задать переменные окружения SDKEY и SD_BASE_URL")
|
raise ValueError("Необходимо задать переменные окружения SDKEY и SD_BASE_URL")
|
||||||
|
|
||||||
# --- Paths ---
|
# --- Paths ---
|
||||||
|
# Директория для хранения лог-файлов
|
||||||
|
LOG_PATH = os.getenv("LOGPATH", ".")
|
||||||
DB_PATH = os.getenv("BDPATH", ".") # По умолчанию - текущая папка
|
DB_PATH = os.getenv("BDPATH", ".") # По умолчанию - текущая папка
|
||||||
DB_NAME = "fiscals.db"
|
DB_NAME = "fiscals.db"
|
||||||
DB_FULL_PATH = os.path.join(DB_PATH, DB_NAME)
|
DB_FULL_PATH = os.path.join(DB_PATH, DB_NAME)
|
||||||
@@ -32,3 +33,12 @@ FIND_WORKSTATIONS_URL = f"{SD_BASE_URL}/find/objectBase$Workstation"
|
|||||||
CREATE_FR_URL = f"{SD_BASE_URL}/create-m2m/objectBase$FR"
|
CREATE_FR_URL = f"{SD_BASE_URL}/create-m2m/objectBase$FR"
|
||||||
# URL для редактирования будет принимать UUID объекта
|
# URL для редактирования будет принимать UUID объекта
|
||||||
EDIT_FR_URL_TEMPLATE = f"{SD_BASE_URL}/edit/{{uuid}}"
|
EDIT_FR_URL_TEMPLATE = f"{SD_BASE_URL}/edit/{{uuid}}"
|
||||||
|
|
||||||
|
# --- Testing Configuration ---
|
||||||
|
# Установите в "True" для режима "сухого запуска" (dry run).
|
||||||
|
# В этом режиме запросы на изменение/создание не будут отправляться в SD,
|
||||||
|
# а будут сохраняться в локальные файлы для анализа.
|
||||||
|
DRY_RUN = os.getenv('DRY_RUN', 'True').lower() in ('true', '1', 't')
|
||||||
|
|
||||||
|
# Путь для сохранения тестовых выходных данных
|
||||||
|
TEST_OUTPUT_PATH = os.path.join(os.path.dirname(__file__), 'test_output')
|
||||||
16
database.py
16
database.py
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
# Настройка логирования для модуля БД
|
# Настройка логирования для модуля БД
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -15,6 +16,16 @@ class DatabaseManager:
|
|||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.conn = None
|
self.conn = None
|
||||||
|
|
||||||
|
# Проверяем, существует ли директория для файла БД, и создаем ее, если нет.
|
||||||
|
db_dir = os.path.dirname(self.db_path)
|
||||||
|
if db_dir and not os.path.exists(db_dir):
|
||||||
|
try:
|
||||||
|
os.makedirs(db_dir)
|
||||||
|
log.info(f"Создана директория для базы данных: {db_dir}")
|
||||||
|
except OSError as e:
|
||||||
|
log.error(f"Не удалось создать директорию для БД {db_dir}: {e}")
|
||||||
|
raise # Перевыбрасываем исключение, так как без директории работа невозможна
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Открывает соединение с БД при входе в контекстный менеджер."""
|
"""Открывает соединение с БД при входе в контекстный менеджер."""
|
||||||
try:
|
try:
|
||||||
@@ -104,7 +115,8 @@ class DatabaseManager:
|
|||||||
uuid TEXT PRIMARY KEY,
|
uuid TEXT PRIMARY KEY,
|
||||||
owner_uuid TEXT,
|
owner_uuid TEXT,
|
||||||
clean_anydesk_id TEXT,
|
clean_anydesk_id TEXT,
|
||||||
clean_teamviewer_id TEXT
|
clean_teamviewer_id TEXT,
|
||||||
|
lastModifiedDate TEXT
|
||||||
)""")
|
)""")
|
||||||
|
|
||||||
# Новая таблица для кэширования UUID справочников
|
# Новая таблица для кэширования UUID справочников
|
||||||
@@ -122,7 +134,7 @@ class DatabaseManager:
|
|||||||
log.info(f"Очистка таблиц: {', '.join(table_names)}")
|
log.info(f"Очистка таблиц: {', '.join(table_names)}")
|
||||||
for table in table_names:
|
for table in table_names:
|
||||||
# Проверяем, что имя таблицы "безопасное"
|
# Проверяем, что имя таблицы "безопасное"
|
||||||
if table.isalnum():
|
if all(c.isalnum() or c == '_' for c in table):
|
||||||
self._execute_query(f"DELETE FROM {table}")
|
self._execute_query(f"DELETE FROM {table}")
|
||||||
else:
|
else:
|
||||||
log.warning(f"Попытка очистить таблицу с некорректным именем: {table}")
|
log.warning(f"Попытка очистить таблицу с некорректным именем: {table}")
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 25Ф",
|
|
||||||
"serialNumber": "00105707796831",
|
|
||||||
"RNM": "0004421585034085",
|
|
||||||
"organizationName": "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ \"МЕГА СЕРВИС\"",
|
|
||||||
"fn_serial": "9287440301117169",
|
|
||||||
"datetime_reg": "2021-06-17 17:01:00",
|
|
||||||
"dateTime_end": "2024-07-01 00:00:00",
|
|
||||||
"ofdName": "АО <КАЛУГА АСТРАЛ>",
|
|
||||||
"bootVersion": "3.0.8319",
|
|
||||||
"ffdVersion": "105",
|
|
||||||
"INN": "7730255999",
|
|
||||||
"attribute_excise": "False",
|
|
||||||
"attribute_marked": "Не поддерживается в текущей версии драйвера",
|
|
||||||
"fnExecution": "Не поддерживается в текущей версии драйвера",
|
|
||||||
"hostname": "13CASH08",
|
|
||||||
"url_rms": "https://aom-himki.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "1050831481",
|
|
||||||
"anydesk_id": "334171076",
|
|
||||||
"total_space_sys": "69.48 Gb",
|
|
||||||
"free_space_sys": "45.86 Gb",
|
|
||||||
"current_time": "2024-05-08 20:42:24"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 30Ф",
|
|
||||||
"serialNumber": "00106126844935",
|
|
||||||
"RNM": "0007195642054348",
|
|
||||||
"organizationName": "ИП Зайцев Егор Владимирович",
|
|
||||||
"fn_serial": "7281440500463082",
|
|
||||||
"datetime_reg": "2023-04-24 21:05:00",
|
|
||||||
"dateTime_end": "2024-06-07 00:00:00",
|
|
||||||
"ofdName": "ООО Такском",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "110120364802",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-3 ",
|
|
||||||
"hostname": "AVE-SHAVE-GK",
|
|
||||||
"url_rms": "https://ave-shawe.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "None",
|
|
||||||
"anydesk_id": "391033027",
|
|
||||||
"total_space_sys": "58.13 Gb",
|
|
||||||
"free_space_sys": "27.29 Gb",
|
|
||||||
"current_time": "2024-05-09 14:15:56"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 55Ф",
|
|
||||||
"serialNumber": "00106202123911",
|
|
||||||
"RNM": "0002593555046068",
|
|
||||||
"organizationName": "ООО \"ТЕАТРАЛЬНАЯ\"",
|
|
||||||
"fn_serial": "7284440500258734",
|
|
||||||
"datetime_reg": "2023-06-30 14:08:00",
|
|
||||||
"dateTime_end": "2024-08-13 00:00:00",
|
|
||||||
"ofdName": "сбис",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "6827024224 ",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ав15-3 ",
|
|
||||||
"hostname": "User-PC80",
|
|
||||||
"url_rms": "https://chainik-cloud.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "593526432",
|
|
||||||
"anydesk_id": "1919438899",
|
|
||||||
"total_space_sys": "111.25 Gb",
|
|
||||||
"free_space_sys": "73.01 Gb",
|
|
||||||
"current_time": "2024-05-07 18:20:55"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ FPrint-22ПТК",
|
|
||||||
"serialNumber": "00106302050149",
|
|
||||||
"RNM": "0000839196015549",
|
|
||||||
"organizationName": "ИП АВАНЕСОВА НАТАЛЬЯ НОРДЕЕВНА",
|
|
||||||
"fn_serial": "7281440501031166",
|
|
||||||
"datetime_reg": "2023-05-28 10:18:00",
|
|
||||||
"dateTime_end": "2024-07-11 00:00:00",
|
|
||||||
"ofdName": "ООО \"Такском\"",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "507901506303",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-3 ",
|
|
||||||
"hostname": "RASSKAZOVKA_BAGET",
|
|
||||||
"url_rms": "http://88.99.60.29:8187/resto",
|
|
||||||
"teamviever_id": "1145765525",
|
|
||||||
"anydesk_id": "952794502",
|
|
||||||
"total_space_sys": "59.62 Gb",
|
|
||||||
"free_space_sys": "7.44 Gb",
|
|
||||||
"current_time": "2024-05-06 02:02:23"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ FPrint-22ПТК",
|
|
||||||
"serialNumber": "00106309062146",
|
|
||||||
"RNM": "0005464060042782",
|
|
||||||
"organizationName": "ИП ИЛЬЕНКО МАКСИМ МИХАЙЛОВИЧ",
|
|
||||||
"fn_serial": "9287440301158465",
|
|
||||||
"datetime_reg": "2021-05-21 16:35:00",
|
|
||||||
"dateTime_end": "2024-06-04 00:00:00",
|
|
||||||
"ofdName": "ООО \"Эвотор ОФД\"",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "105",
|
|
||||||
"INN": "501300190904",
|
|
||||||
"attribute_excise": "False",
|
|
||||||
"attribute_marked": "Не поддерживается в текущей версии драйвера",
|
|
||||||
"fnExecution": "Не поддерживается в текущей версии драйвера",
|
|
||||||
"hostname": "Kaldis_ST-8",
|
|
||||||
"url_rms": "https://kaldis-st-8.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "130045574",
|
|
||||||
"anydesk_id": "856695376",
|
|
||||||
"total_space_sys": "59.62 Gb",
|
|
||||||
"free_space_sys": "26.18 Gb",
|
|
||||||
"current_time": "2024-05-09 01:11:30"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ FPrint-22ПТК",
|
|
||||||
"serialNumber": "00106309239401",
|
|
||||||
"RNM": "0005463930014863",
|
|
||||||
"organizationName": "ИП ИЛЬЕНКО МАКСИМ МИХАЙЛОВИЧ",
|
|
||||||
"fn_serial": "9287440301158440",
|
|
||||||
"datetime_reg": "2021-05-21 15:58:00",
|
|
||||||
"dateTime_end": "2024-06-04 00:00:00",
|
|
||||||
"ofdName": "ООО \"Эвотор ОФД\"",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "105",
|
|
||||||
"INN": "501300190904",
|
|
||||||
"attribute_excise": "False",
|
|
||||||
"attribute_marked": "False",
|
|
||||||
"fnExecution": "",
|
|
||||||
"hostname": "Kaldis_ST-7",
|
|
||||||
"url_rms": "https://kaldis-st-7.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "130042147",
|
|
||||||
"anydesk_id": "595633117",
|
|
||||||
"total_space_sys": "59.62 Gb",
|
|
||||||
"free_space_sys": "20.22 Gb",
|
|
||||||
"current_time": "2024-05-06 01:41:41"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ FPrint-22ПТК",
|
|
||||||
"serialNumber": "00106309300916",
|
|
||||||
"RNM": "0006138955056715",
|
|
||||||
"organizationName": "ИП ИЛЬЕНКО МАРИЯ ВЛАДИМИРОВНА",
|
|
||||||
"fn_serial": "7281440701572965",
|
|
||||||
"datetime_reg": "2024-04-23 21:49:00",
|
|
||||||
"dateTime_end": "2025-06-07 00:00:00",
|
|
||||||
"ofdName": "ООО \"Эвотор ОФД\"",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "504005415507",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-3 ",
|
|
||||||
"hostname": "POS-Lianozovo",
|
|
||||||
"url_rms": "https://kaldis-lianozovo.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "1493401222",
|
|
||||||
"anydesk_id": "668050369",
|
|
||||||
"total_space_sys": "59.14 Gb",
|
|
||||||
"free_space_sys": "7.49 Gb",
|
|
||||||
"current_time": "2024-05-09 23:16:42"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ FPrint-22ПТК",
|
|
||||||
"serialNumber": "00106309330871",
|
|
||||||
"RNM": "0002792971023790",
|
|
||||||
"organizationName": "ООО \"ФинЦентр\"",
|
|
||||||
"fn_serial": "9961440300055208",
|
|
||||||
"datetime_reg": "2023-02-23 09:04:00",
|
|
||||||
"dateTime_end": "2026-03-09 00:00:00",
|
|
||||||
"ofdName": "АО <Калуга Астрал>",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "7743884031 ",
|
|
||||||
"attribute_excise": "False",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.1М исполнение Ин36-1М ",
|
|
||||||
"hostname": "POS-W10",
|
|
||||||
"url_rms": "https://press-bar-moskva-m-servis.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "1231759409",
|
|
||||||
"anydesk_id": "407482388",
|
|
||||||
"total_space_sys": "59.62 Gb",
|
|
||||||
"free_space_sys": "28.14 Gb",
|
|
||||||
"current_time": "2024-05-06 12:25:56"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 77Ф",
|
|
||||||
"serialNumber": "00106905442664",
|
|
||||||
"RNM": "0001326280045003",
|
|
||||||
"organizationName": "АВАНЕСОВА НАТАЛЬЯ НОРДЕЕВНА",
|
|
||||||
"fn_serial": "7380440700519749",
|
|
||||||
"datetime_reg": "2024-04-05 14:58:00",
|
|
||||||
"dateTime_end": "2025-05-20 00:00:00",
|
|
||||||
"ofdName": "ООО <Такском>",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "507901506303",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-4 ",
|
|
||||||
"hostname": "DESKTOP-IUSVUSE",
|
|
||||||
"url_rms": "http://88.99.60.29:8187/resto",
|
|
||||||
"teamviever_id": "1284989181",
|
|
||||||
"anydesk_id": "None",
|
|
||||||
"total_space_sys": "59.04 Gb",
|
|
||||||
"free_space_sys": "18.64 Gb",
|
|
||||||
"current_time": "2024-05-06 02:08:13"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 20Ф",
|
|
||||||
"serialNumber": "00108100739722",
|
|
||||||
"RNM": "0001781789038839",
|
|
||||||
"organizationName": "ИП Пилин Игорь Андреевич",
|
|
||||||
"fn_serial": "9961440300916272",
|
|
||||||
"datetime_reg": "2022-08-18 08:36:00",
|
|
||||||
"dateTime_end": "2025-09-01 00:00:00",
|
|
||||||
"ofdName": "АО \"ЭСК\"",
|
|
||||||
"bootVersion": "3.0.4253",
|
|
||||||
"ffdVersion": "105",
|
|
||||||
"INN": "344309628497",
|
|
||||||
"attribute_excise": "False",
|
|
||||||
"attribute_marked": "False",
|
|
||||||
"fnExecution": "",
|
|
||||||
"hostname": "WIN-DMS6GB0U6TO",
|
|
||||||
"url_rms": "https://3-sushi-mira.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "1285328750",
|
|
||||||
"anydesk_id": "998297587",
|
|
||||||
"total_space_sys": "111.38 Gb",
|
|
||||||
"free_space_sys": "63.19 Gb",
|
|
||||||
"current_time": "2024-05-09 07:08:03"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 20Ф",
|
|
||||||
"serialNumber": "00108129540393",
|
|
||||||
"RNM": "0007978093023550",
|
|
||||||
"organizationName": "ИП Ким Вячеслав",
|
|
||||||
"fn_serial": "7380440700292820",
|
|
||||||
"datetime_reg": "2024-04-11 15:18:00",
|
|
||||||
"dateTime_end": "2025-05-26 00:00:00",
|
|
||||||
"ofdName": "ООО ПЕТЕР-СЕРВИС Спецтехнологии",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "650125159002",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-4 ",
|
|
||||||
"hostname": "KASSA2-KHABAR",
|
|
||||||
"url_rms": "https://mirine-brosko-habarovsk.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "None",
|
|
||||||
"anydesk_id": "1063125576",
|
|
||||||
"total_space_sys": "59.09 Gb",
|
|
||||||
"free_space_sys": "34.89 Gb",
|
|
||||||
"current_time": "2024-05-07 12:27:50"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 27Ф",
|
|
||||||
"serialNumber": "00108722684571",
|
|
||||||
"RNM": "0005975228045176",
|
|
||||||
"organizationName": "Индивидуальный предприниматель Аванесова Наталья Нордеевна",
|
|
||||||
"fn_serial": "7380440700402306",
|
|
||||||
"datetime_reg": "2024-04-05 14:43:00",
|
|
||||||
"dateTime_end": "2025-05-20 00:00:00",
|
|
||||||
"ofdName": "ООО Такском",
|
|
||||||
"bootVersion": "5.10.0",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "507901506303",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-4 ",
|
|
||||||
"hostname": "Burkina-kofe-NEW",
|
|
||||||
"url_rms": "http://88.99.60.29:8187/resto",
|
|
||||||
"teamviever_id": "650019532",
|
|
||||||
"anydesk_id": "1562660748",
|
|
||||||
"total_space_sys": "59.62 Gb",
|
|
||||||
"free_space_sys": "34.64 Gb",
|
|
||||||
"current_time": "2024-05-06 02:16:47"
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 27Ф",
|
|
||||||
"serialNumber": "00108729580581",
|
|
||||||
"RNM": "0007037066025713",
|
|
||||||
"organizationName": "ИП ДАВЛАТОВ МИРЗОШО РАХМАТШОЕВИЧ",
|
|
||||||
"fn_serial": "7281440500179066",
|
|
||||||
"datetime_reg": "2023-02-16 10:39:00",
|
|
||||||
"dateTime_end": "2024-05-31 00:00:00",
|
|
||||||
"ofdName": "ООО Эвотор ОФД",
|
|
||||||
"bootVersion": "5.7.13",
|
|
||||||
"ffdVersion": "105",
|
|
||||||
"INN": "670602568722",
|
|
||||||
"fnExecution": "",
|
|
||||||
"attribute_podakciz": "False",
|
|
||||||
"attribute_marked": "False"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 22 v2 Ф",
|
|
||||||
"serialNumber": "00109522991414",
|
|
||||||
"RNM": "0007882610017388",
|
|
||||||
"organizationName": "ИП СОКЛАКОВА АННА СЕРГЕЕВНА",
|
|
||||||
"fn_serial": "7380440700100735",
|
|
||||||
"datetime_reg": "2024-02-29 13:10:00",
|
|
||||||
"dateTime_end": "2025-04-14 00:00:00",
|
|
||||||
"ofdName": "ООО Такском",
|
|
||||||
"bootVersion": "5.8.17",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"INN": "771315163893",
|
|
||||||
"attribute_excise": "True",
|
|
||||||
"attribute_marked": "True",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-4 ",
|
|
||||||
"hostname": "VESTERDAM",
|
|
||||||
"url_rms": "https://pokolenie-kofe-vesterdam.iiko.it:443/resto",
|
|
||||||
"teamviever_id": "743932094",
|
|
||||||
"anydesk_id": "369655451",
|
|
||||||
"total_space_sys": "58.13 Gb",
|
|
||||||
"free_space_sys": "21.30 Gb",
|
|
||||||
"current_time": "2024-05-09 10:38:44"
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 22 v2 Ф",
|
|
||||||
"serialNumber": "00109529077045",
|
|
||||||
"RNM": "0006899083013508",
|
|
||||||
"organizationName": "ООО \"ГАВАНА\"",
|
|
||||||
"fn_serial": "7281440701652919",
|
|
||||||
"datetime_reg": "2024-03-20 11:43:00",
|
|
||||||
"dateTime_end": "2025-07-03 00:00:00",
|
|
||||||
"ofdName": "ООО Эвотор ОФД",
|
|
||||||
"bootVersion": "5.8.20",
|
|
||||||
"ffdVersion": "105",
|
|
||||||
"INN": "5904388065 ",
|
|
||||||
"fnExecution": "None",
|
|
||||||
"attribute_podakciz": "True",
|
|
||||||
"attribute_marked": "False"
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 22 v2 Ф",
|
|
||||||
"serialNumber": "00109525422090",
|
|
||||||
"RNM": "0000000004454545",
|
|
||||||
"organizationName": "ООО \"Предприятие\"",
|
|
||||||
"fn_serial": "7380440700425457",
|
|
||||||
"datetime_reg": "2024-04-02 03:35:00",
|
|
||||||
"dateTime_end": "2079-12-31 00:00:00",
|
|
||||||
"ofdName": "ООО \"Эвотор ОФД\"",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"fnExecution": "Эмулятор ФН с поддержкой ФФД 1.2 ",
|
|
||||||
"INN": "1111222233 "
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 22 v2 Ф",
|
|
||||||
"serialNumber": "00109525422090",
|
|
||||||
"RNM": "0000000001032218",
|
|
||||||
"organizationName": "ООО \"Предприятие\"",
|
|
||||||
"fn_serial": "7380440700425457",
|
|
||||||
"datetime_reg": "2024-04-02 03:35:00",
|
|
||||||
"dateTime_end": "2057-12-31 00:00:00",
|
|
||||||
"ofdName": "ООО \"Эвотор ОФД\"",
|
|
||||||
"bootVersion": "5.8.100",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"fnExecution": "Эмулятор ФН с поддержкой ФФД 1.2 ",
|
|
||||||
"INN": "1111222233 "
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"modelName": "АТОЛ 27Ф",
|
|
||||||
"serialNumber": "00108722318182",
|
|
||||||
"RNM": "0006025947045252",
|
|
||||||
"organizationName": "ООО АЛЕКС-СЕРВИС",
|
|
||||||
"fn_serial": "7281440701487984",
|
|
||||||
"datetime_reg": "2024-02-27 09:13:00",
|
|
||||||
"dateTime_end": "2025-04-12 00:00:00",
|
|
||||||
"ofdName": "ООО Такском",
|
|
||||||
"bootVersion": "5.10.0",
|
|
||||||
"ffdVersion": "120",
|
|
||||||
"fnExecution": "ФН-1.2 исполнение Ин15-3",
|
|
||||||
"INN": "5258144870 "
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
# ftp_parser.py
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from dateutil import parser
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ def process_json_files(directory: str) -> List[Dict]:
|
|||||||
"""
|
"""
|
||||||
Сканирует директорию, читает все .json файлы и извлекает из них данные
|
Сканирует директорию, читает все .json файлы и извлекает из них данные
|
||||||
о фискальных регистраторах в стандартизированном формате.
|
о фискальных регистраторах в стандартизированном формате.
|
||||||
|
Файлы, старше 21 дня (по полю 'current_time'), игнорируются.
|
||||||
|
|
||||||
:param directory: Путь к директории с JSON файлами.
|
:param directory: Путь к директории с JSON файлами.
|
||||||
:return: Список словарей, где каждый словарь представляет один ФР.
|
:return: Список словарей, где каждый словарь представляет один ФР.
|
||||||
@@ -22,6 +24,9 @@ def process_json_files(directory: str) -> List[Dict]:
|
|||||||
all_fr_data = []
|
all_fr_data = []
|
||||||
log.info(f"Начинаю обработку JSON файлов из директории: {directory}")
|
log.info(f"Начинаю обработку JSON файлов из директории: {directory}")
|
||||||
|
|
||||||
|
# Пороговая дата: всё что старше, считаем неактуальным
|
||||||
|
freshness_threshold = datetime.now() - timedelta(days=21)
|
||||||
|
|
||||||
for filename in os.listdir(directory):
|
for filename in os.listdir(directory):
|
||||||
if filename.lower().endswith('.json'):
|
if filename.lower().endswith('.json'):
|
||||||
file_path = os.path.join(directory, filename)
|
file_path = os.path.join(directory, filename)
|
||||||
@@ -29,6 +34,26 @@ def process_json_files(directory: str) -> List[Dict]:
|
|||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
|
current_time_str = data.get('current_time')
|
||||||
|
if not current_time_str:
|
||||||
|
log.warning(f"Пропущен файл {filename}: отсутствует поле 'current_time' для проверки актуальности.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_datetime = parser.parse(current_time_str)
|
||||||
|
# Приводим к aware-объекту с локальной таймзоной, если он naive, для корректного сравнения
|
||||||
|
if file_datetime.tzinfo is None:
|
||||||
|
file_datetime = file_datetime.astimezone()
|
||||||
|
|
||||||
|
# Сравниваем с пороговым значением (пороговое значение тоже делаем aware)
|
||||||
|
if file_datetime < freshness_threshold.astimezone(file_datetime.tzinfo):
|
||||||
|
log.warning(f"Пропущен файл {filename}: данные неактуальны (старше 21 дня). "
|
||||||
|
f"Дата файла: {file_datetime.strftime('%Y-%m-%d')}")
|
||||||
|
continue
|
||||||
|
except parser.ParserError:
|
||||||
|
log.error(f"Не удалось распознать дату в поле 'current_time' в файле {filename}: '{current_time_str}'")
|
||||||
|
continue
|
||||||
|
|
||||||
if 'serialNumber' not in data or not data['serialNumber']:
|
if 'serialNumber' not in data or not data['serialNumber']:
|
||||||
log.warning(f"Пропущен файл {filename}: отсутствует serialNumber.")
|
log.warning(f"Пропущен файл {filename}: отсутствует serialNumber.")
|
||||||
continue
|
continue
|
||||||
|
|||||||
54
main.py
54
main.py
@@ -1,8 +1,8 @@
|
|||||||
# main.py
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
import schedule
|
import schedule
|
||||||
|
|
||||||
import config
|
import config
|
||||||
@@ -11,15 +11,53 @@ from sd_api import ServiceDeskClient
|
|||||||
from sync_logic import Synchronizer
|
from sync_logic import Synchronizer
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
"""Настраивает базовую конфигурацию логирования."""
|
"""
|
||||||
logging.basicConfig(
|
Настраивает продвинутую конфигурацию логирования с ротацией файлов
|
||||||
level=logging.INFO,
|
и разными уровнями для консоли и файла.
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
"""
|
||||||
stream=sys.stdout, # Вывод логов в stdout
|
# 1. Устанавливаем корневому логгеру самый низкий уровень (DEBUG).
|
||||||
|
# Это позволяет хэндлерам самим решать, что пропускать.
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# 2. Создаем директорию для логов, если она не существует.
|
||||||
|
try:
|
||||||
|
if not os.path.exists(config.LOG_PATH):
|
||||||
|
os.makedirs(config.LOG_PATH)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"Ошибка создания директории для логов {config.LOG_PATH}: {e}")
|
||||||
|
# В случае ошибки выходим, т.к. логирование в файл не будет работать
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 3. Настраиваем форматтер для логов.
|
||||||
|
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
# 4. Настраиваем хэндлер для вывода в консоль (уровень INFO).
|
||||||
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
|
console_handler.setLevel(logging.INFO)
|
||||||
|
console_handler.setFormatter(log_formatter)
|
||||||
|
|
||||||
|
# 5. Настраиваем хэндлер для записи в файл с ежедневной ротацией (уровень DEBUG).
|
||||||
|
# Файлы будут вида sync_service.log, sync_service.log.2023-10-27 и т.д.
|
||||||
|
log_file = os.path.join(config.LOG_PATH, 'sync_service.log')
|
||||||
|
file_handler = logging.handlers.TimedRotatingFileHandler(
|
||||||
|
log_file,
|
||||||
|
when='midnight', # Ротация в полночь
|
||||||
|
interval=1, # Каждый день
|
||||||
|
backupCount=7, # Хранить 7 старых файлов
|
||||||
|
encoding='utf-8'
|
||||||
)
|
)
|
||||||
# Отключаем слишком "болтливые" логи от сторонних библиотек
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
file_handler.setFormatter(log_formatter)
|
||||||
|
|
||||||
|
# 6. Добавляем оба хэндлера к корневому логгеру.
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# 7. Отключаем слишком "болтливые" логи от сторонних библиотек.
|
||||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("schedule").setLevel(logging.INFO)
|
||||||
|
|
||||||
def job():
|
def job():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ python-dotenv
|
|||||||
requests
|
requests
|
||||||
schedule
|
schedule
|
||||||
python-dateutil
|
python-dateutil
|
||||||
|
urllib3
|
||||||
85
sd_api.py
85
sd_api.py
@@ -3,7 +3,11 @@
|
|||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
# Импортируем нашу конфигурацию
|
# Импортируем нашу конфигурацию
|
||||||
import config
|
import config
|
||||||
@@ -17,6 +21,7 @@ class ServiceDeskAPIError(Exception):
|
|||||||
class ServiceDeskClient:
|
class ServiceDeskClient:
|
||||||
"""
|
"""
|
||||||
Клиент для взаимодействия с REST API ServiceDesk.
|
Клиент для взаимодействия с REST API ServiceDesk.
|
||||||
|
Оснащен механизмом повторных запросов для повышения отказоустойчивости.
|
||||||
"""
|
"""
|
||||||
def __init__(self, access_key: str, base_url: str):
|
def __init__(self, access_key: str, base_url: str):
|
||||||
if not access_key or not base_url:
|
if not access_key or not base_url:
|
||||||
@@ -27,36 +32,65 @@ class ServiceDeskClient:
|
|||||||
# Устанавливаем accessKey как параметр по умолчанию для всех запросов
|
# Устанавливаем accessKey как параметр по умолчанию для всех запросов
|
||||||
self.session.params = {'accessKey': access_key}
|
self.session.params = {'accessKey': access_key}
|
||||||
|
|
||||||
|
# --- НАСТРОЙКА ЛОГИКИ ПОВТОРНЫХ ЗАПРОСОВ ---
|
||||||
|
retry_strategy = Retry(
|
||||||
|
total=3, # Общее количество повторных попыток
|
||||||
|
backoff_factor=1, # Множитель для задержки (sleep_time = {backoff_factor} * (2 ** ({number of total retries} - 1)))
|
||||||
|
# Задержки будут примерно 0.5с, 1с, 2с
|
||||||
|
status_forcelist=[500, 502, 503, 504], # Коды, которые нужно повторять
|
||||||
|
allowed_methods=["POST", "GET"] # Методы, для которых будет работать retry. API использует POST даже для поиска.
|
||||||
|
)
|
||||||
|
# Создаем адаптер с нашей стратегией
|
||||||
|
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||||
|
|
||||||
|
# "Монтируем" адаптер для всех http и https запросов
|
||||||
|
self.session.mount("https://", adapter)
|
||||||
|
self.session.mount("http://", adapter)
|
||||||
|
log.info("Клиент ServiceDesk инициализирован с логикой повторных запросов (retries).")
|
||||||
|
|
||||||
def _make_request(self, method: str, url: str, params: Dict = None, json_data: Dict = None) -> Any:
|
def _make_request(self, method: str, url: str, params: Dict = None, json_data: Dict = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Внутренний метод для выполнения HTTP-запросов.
|
Внутренний метод для выполнения HTTP-запросов.
|
||||||
|
Теперь он автоматически использует логику retry, настроенную в __init__.
|
||||||
|
|
||||||
:param method: HTTP метод ('GET', 'POST', etc.)
|
:param method: HTTP метод ('GET', 'POST', etc.)
|
||||||
:param url: Полный URL для запроса.
|
:param url: Полный URL для запроса.
|
||||||
:param params: Дополнительные параметры URL (кроме accessKey).
|
:param params: Дополнительные параметры URL (кроме accessKey).
|
||||||
:param json_data: Тело запроса в формате JSON для POST/PUT.
|
:param json_data: Тело запроса в формате JSON для POST/PUT.
|
||||||
:return: Ответ сервера в виде JSON.
|
:return: Ответ сервера в виде JSON.
|
||||||
:raises ServiceDeskAPIError: в случае ошибки API или сети.
|
:raises ServiceDeskAPIError: в случае ошибки API или сети ПОСЛЕ всех попыток.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Теперь этот вызов будет автоматически повторяться в случае сбоев
|
||||||
response = self.session.request(method, url, params=params, json=json_data, timeout=30)
|
response = self.session.request(method, url, params=params, json=json_data, timeout=30)
|
||||||
|
|
||||||
|
# Проверяем на ошибки ПОСЛЕ успешного выполнения запроса (или исчерпания попыток)
|
||||||
response.raise_for_status() # Вызовет исключение для кодов 4xx/5xx
|
response.raise_for_status() # Вызовет исключение для кодов 4xx/5xx
|
||||||
|
|
||||||
# Некоторые ответы могут быть пустыми (например, при успешном редактировании)
|
# Некоторые ответы могут быть пустыми (например, при успешном редактировании - 204)
|
||||||
if response.status_code == 204 or not response.text:
|
# или успешном создании (201)
|
||||||
|
if response.status_code in [204, 201] or not response.text:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
|
# Эта ошибка возникнет, если после всех попыток сервер все равно вернул 4xx или 5xx
|
||||||
error_message = f"HTTP Error: {e.response.status_code} for URL {url}. Response: {e.response.text}"
|
error_message = f"HTTP Error: {e.response.status_code} for URL {url}. Response: {e.response.text}"
|
||||||
log.error(error_message)
|
log.error(error_message)
|
||||||
raise ServiceDeskAPIError(error_message) from e
|
raise ServiceDeskAPIError(error_message) from e
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
|
# Эта ошибка возникнет, если после всех попыток не удалось подключиться к серверу (например, DNS или таймаут)
|
||||||
error_message = f"Request failed for URL {url}: {e}"
|
error_message = f"Request failed for URL {url}: {e}"
|
||||||
log.error(error_message)
|
log.error(error_message)
|
||||||
raise ServiceDeskAPIError(error_message) from e
|
raise ServiceDeskAPIError(error_message) from e
|
||||||
|
|
||||||
|
def _ensure_test_output_dir(self):
|
||||||
|
"""Проверяет и создает директорию для тестовых файлов."""
|
||||||
|
if not os.path.exists(config.TEST_OUTPUT_PATH):
|
||||||
|
os.makedirs(config.TEST_OUTPUT_PATH)
|
||||||
|
log.info(f"Создана директория для тестовых данных: {config.TEST_OUTPUT_PATH}")
|
||||||
|
|
||||||
def get_all_frs(self) -> List[Dict]:
|
def get_all_frs(self) -> List[Dict]:
|
||||||
"""Получает список всех фискальных регистраторов."""
|
"""Получает список всех фискальных регистраторов."""
|
||||||
log.info("Запрос списка всех ФР из ServiceDesk...")
|
log.info("Запрос списка всех ФР из ServiceDesk...")
|
||||||
@@ -65,17 +99,18 @@ class ServiceDeskClient:
|
|||||||
}
|
}
|
||||||
frs = self._make_request('POST', config.FIND_FRS_URL, params=params)
|
frs = self._make_request('POST', config.FIND_FRS_URL, params=params)
|
||||||
log.info(f"Получено {len(frs)} записей о ФР.")
|
log.info(f"Получено {len(frs)} записей о ФР.")
|
||||||
return frs
|
# frs может быть None если ответ пустой, вернем пустой список для консистентности
|
||||||
|
return frs or []
|
||||||
|
|
||||||
def get_all_workstations(self) -> List[Dict]:
|
def get_all_workstations(self) -> List[Dict]:
|
||||||
"""Получает список всех рабочих станций."""
|
"""Получает список всех рабочих станций."""
|
||||||
log.info("Запрос списка всех рабочих станций из ServiceDesk...")
|
log.info("Запрос списка всех рабочих станций из ServiceDesk...")
|
||||||
params = {
|
params = {
|
||||||
'attrs': 'UUID,owner,AnyDesk,Teamviewer'
|
'attrs': 'UUID,owner,AnyDesk,Teamviewer,lastModifiedDate'
|
||||||
}
|
}
|
||||||
workstations = self._make_request('POST', config.FIND_WORKSTATIONS_URL, params=params)
|
workstations = self._make_request('POST', config.FIND_WORKSTATIONS_URL, params=params)
|
||||||
log.info(f"Получено {len(workstations)} записей о рабочих станциях.")
|
log.info(f"Получено {len(workstations)} записей о рабочих станциях.")
|
||||||
return workstations
|
return workstations or []
|
||||||
|
|
||||||
def get_lookup_values(self, metaclass: str) -> List[Dict]:
|
def get_lookup_values(self, metaclass: str) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
@@ -89,19 +124,34 @@ class ServiceDeskClient:
|
|||||||
params = {'attrs': 'UUID,title'}
|
params = {'attrs': 'UUID,title'}
|
||||||
lookups = self._make_request('POST', url, params=params)
|
lookups = self._make_request('POST', url, params=params)
|
||||||
log.info(f"Получено {len(lookups)} значений для {metaclass}.")
|
log.info(f"Получено {len(lookups)} значений для {metaclass}.")
|
||||||
return lookups
|
return lookups or []
|
||||||
|
|
||||||
def update_fr(self, uuid: str, data: Dict) -> None:
|
def update_fr(self, uuid: str, data: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Обновляет существующий фискальный регистратор.
|
Обновляет существующий фискальный регистратор.
|
||||||
|
В режиме DRY_RUN сохраняет данные в файл.
|
||||||
|
|
||||||
:param uuid: UUID объекта для редактирования.
|
:param uuid: UUID объекта для редактирования.
|
||||||
:param data: Словарь с полями для обновления.
|
:param data: Словарь с полями для обновления.
|
||||||
"""
|
"""
|
||||||
|
if config.DRY_RUN:
|
||||||
|
self._ensure_test_output_dir()
|
||||||
|
filename = f"update_{uuid}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
|
filepath = os.path.join(config.TEST_OUTPUT_PATH, filename)
|
||||||
|
|
||||||
|
# Сохраняем и URL, и параметры для полного понимания
|
||||||
|
output_data = {
|
||||||
|
"action": "UPDATE",
|
||||||
|
"target_uuid": uuid,
|
||||||
|
"payload": data
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(output_data, f, indent=4, ensure_ascii=False)
|
||||||
|
log.warning(f"[DRY RUN] Пропущено обновление {uuid}. Данные сохранены в {filepath}")
|
||||||
|
return
|
||||||
|
|
||||||
log.info(f"Обновление объекта ФР с UUID: {uuid}...")
|
log.info(f"Обновление объекта ФР с UUID: {uuid}...")
|
||||||
# Примечание: старый код использовал POST с параметрами для редактирования.
|
|
||||||
# Если API требует form-encoded data, а не JSON, нужно использовать `data=data` вместо `json=data`.
|
|
||||||
# Судя по вашему коду, это POST-запрос с параметрами в URL, а не в теле.
|
|
||||||
url = config.EDIT_FR_URL_TEMPLATE.format(uuid=uuid)
|
url = config.EDIT_FR_URL_TEMPLATE.format(uuid=uuid)
|
||||||
self._make_request('POST', url, params=data)
|
self._make_request('POST', url, params=data)
|
||||||
log.info(f"Объект {uuid} успешно обновлен.")
|
log.info(f"Объект {uuid} успешно обновлен.")
|
||||||
@@ -109,10 +159,25 @@ class ServiceDeskClient:
|
|||||||
def create_fr(self, data: Dict) -> Dict:
|
def create_fr(self, data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
Создает новый фискальный регистратор.
|
Создает новый фискальный регистратор.
|
||||||
|
В режиме DRY_RUN сохраняет тело запроса в файл.
|
||||||
|
|
||||||
:param data: Словарь с данными для создания объекта (тело запроса).
|
:param data: Словарь с данными для создания объекта (тело запроса).
|
||||||
:return: JSON-ответ от сервера, содержащий UUID нового объекта.
|
:return: JSON-ответ от сервера, содержащий UUID нового объекта.
|
||||||
"""
|
"""
|
||||||
|
serial_number = data.get('FRSerialNumber', 'unknown_sn')
|
||||||
|
if config.DRY_RUN:
|
||||||
|
self._ensure_test_output_dir()
|
||||||
|
filename = f"create_{serial_number}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
|
filepath = os.path.join(config.TEST_OUTPUT_PATH, filename)
|
||||||
|
|
||||||
|
# Сохраняем только тело запроса
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||||
|
log.warning(f"[DRY RUN] Пропущено создание ФР с S/N {serial_number}. Body запроса сохранено в {filepath}")
|
||||||
|
|
||||||
|
# В режиме dry run возвращаем фейковый ответ, чтобы не сломать вызывающий код
|
||||||
|
return {"action": "DRY_RUN_CREATE", "saved_to": filepath}
|
||||||
|
|
||||||
log.info(f"Создание нового ФР с серийным номером: {data.get('FRSerialNumber')}...")
|
log.info(f"Создание нового ФР с серийным номером: {data.get('FRSerialNumber')}...")
|
||||||
# Параметр для получения UUID в ответе
|
# Параметр для получения UUID в ответе
|
||||||
params = {'attrs': 'UUID'}
|
params = {'attrs': 'UUID'}
|
||||||
|
|||||||
168
sync_logic.py
168
sync_logic.py
@@ -3,7 +3,7 @@
|
|||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
# Импортируем наши модули
|
# Импортируем наши модули
|
||||||
@@ -17,9 +17,7 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def _clean_sd_remote_id(raw_id: Optional[str]) -> Optional[str]:
|
def _clean_sd_remote_id(raw_id: Optional[str]) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Очищает ID удаленного доступа, полученный из ServiceDesk.
|
Очищает ID удаленного доступа, полученный из ServiceDesk..
|
||||||
(Эта функция была ранее в ftp_parser, но перенесена сюда,
|
|
||||||
так как "грязные" ID приходят из SD).
|
|
||||||
"""
|
"""
|
||||||
if not raw_id or raw_id.lower() == 'none':
|
if not raw_id or raw_id.lower() == 'none':
|
||||||
return None
|
return None
|
||||||
@@ -157,9 +155,10 @@ class Synchronizer:
|
|||||||
ws['UUID'],
|
ws['UUID'],
|
||||||
owner['UUID'],
|
owner['UUID'],
|
||||||
_clean_sd_remote_id(ws.get('AnyDesk')),
|
_clean_sd_remote_id(ws.get('AnyDesk')),
|
||||||
_clean_sd_remote_id(ws.get('Teamviewer'))
|
_clean_sd_remote_id(ws.get('Teamviewer')),
|
||||||
|
ws.get('lastModifiedDate')
|
||||||
))
|
))
|
||||||
self.db.bulk_insert('workstations', ['uuid', 'owner_uuid', 'clean_anydesk_id', 'clean_teamviewer_id'], workstations_data)
|
self.db.bulk_insert('workstations', ['uuid', 'owner_uuid', 'clean_anydesk_id', 'clean_teamviewer_id', 'lastModifiedDate'], workstations_data)
|
||||||
|
|
||||||
# 6. Логика синхронизации
|
# 6. Логика синхронизации
|
||||||
self._update_existing_frs()
|
self._update_existing_frs()
|
||||||
@@ -172,53 +171,120 @@ class Synchronizer:
|
|||||||
|
|
||||||
|
|
||||||
def _update_existing_frs(self):
|
def _update_existing_frs(self):
|
||||||
"""Находит и обновляет ФР с отличающимися датами."""
|
"""
|
||||||
log.info("Поиск ФР для обновления даты окончания ФН...")
|
Находит и обновляет ФР с отличающимися датами окончания ФН.
|
||||||
|
Пропускает обновление, если разница в датах более 10 лет.
|
||||||
|
"""
|
||||||
|
log.info("Поиск ФР для обновления...")
|
||||||
|
|
||||||
|
# 1. SQL-запрос для поиска расхождений.
|
||||||
|
# Он объединяет таблицы по serialNumber и выбирает все необходимые поля
|
||||||
|
# сразу, чтобы избежать вложенных циклов.
|
||||||
query = """
|
query = """
|
||||||
SELECT
|
SELECT
|
||||||
pos.serialNumber,
|
pos.serialNumber,
|
||||||
pos.dateTime_end AS pos_date,
|
pos.dateTime_end AS pos_date_end,
|
||||||
pos.datetime_reg AS pos_reg_date,
|
pos.datetime_reg AS pos_date_reg,
|
||||||
pos.fn_serial AS pos_fn_serial,
|
pos.fn_serial AS pos_fn_serial,
|
||||||
pos.RNM AS pos_rnm,
|
pos.RNM AS pos_rnm,
|
||||||
pos.bootVersion AS pos_boot_version,
|
pos.bootVersion AS pos_boot_version,
|
||||||
pos.organizationName AS pos_org_name,
|
pos.organizationName AS pos_org_name,
|
||||||
pos.INN AS pos_inn,
|
pos.INN AS pos_inn,
|
||||||
sd.UUID AS sd_uuid,
|
sd.UUID AS sd_uuid,
|
||||||
sd.dateTime_end AS sd_date
|
sd.dateTime_end AS sd_date_end
|
||||||
FROM pos_fiscals pos
|
FROM pos_fiscals pos
|
||||||
JOIN sd_fiscals sd ON pos.serialNumber = sd.serialNumber
|
JOIN sd_fiscals sd ON pos.serialNumber = sd.serialNumber
|
||||||
WHERE pos.dateTime_end != sd.dateTime_end
|
|
||||||
"""
|
"""
|
||||||
# Дополнительное условие по дате, как в старом коде, можно добавить сюда, если нужно
|
|
||||||
# WHERE date(pos.dateTime_end) > date(sd.dateTime_end, '-65 day') AND pos.dateTime_end != sd.dateTime_end
|
|
||||||
|
|
||||||
records_to_update = self.db._execute_query(query, fetch='all')
|
records_to_check = self.db._execute_query(query, fetch='all')
|
||||||
log.info(f"Найдено {len(records_to_update)} ФР для обновления.")
|
update_counter = 0
|
||||||
|
|
||||||
for rec in records_to_update:
|
#Порог для сравнения дат
|
||||||
# Проверка, что дата действительно новее (избегаем "старых" данных с FTP)
|
archive_delta = timedelta(days=365*10)
|
||||||
# Это простая проверка, можно усложнить, если нужно
|
|
||||||
if parser.parse(rec['pos_date']) < parser.parse(rec['sd_date']):
|
for rec in records_to_check:
|
||||||
log.warning(f"Пропуск обновления для S/N {rec['serialNumber']}: дата с FTP ({rec['pos_date']}) "
|
try:
|
||||||
f"старше, чем в SD ({rec['sd_date']}).")
|
# 2. Приводим даты к одному "знаменателю" - объектам datetime
|
||||||
|
# dateutil.parser отлично справляется с разными форматами
|
||||||
|
# Проверяем, что обе даты существуют, прежде чем их парсить
|
||||||
|
if not rec['pos_date_end'] or not rec['sd_date_end']:
|
||||||
|
log.warning(f"Пропуск сравнения для S/N {rec['serialNumber']}: одна из дат отсутствует.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
legal_name = f"{rec['pos_org_name']} ИНН:{rec['pos_inn']}" if rec['pos_org_name'] and rec['pos_inn'] else "ЗАКОНЧИЛСЯ ФН"
|
pos_date = parser.parse(rec['pos_date_end'])
|
||||||
|
sd_date = parser.parse(rec['sd_date_end'])
|
||||||
|
|
||||||
|
# 3. Сравниваем даты. Если они различаются, готовим обновление.
|
||||||
|
# Проверяем на неравенство. Величина различия не важна.
|
||||||
|
if pos_date != sd_date:
|
||||||
|
# Проверяем, что разница не больше 10 лет
|
||||||
|
if abs(pos_date - sd_date) > archive_delta:
|
||||||
|
log.warning(f"Пропуск сравнения для S/N {rec['serialNumber']}: разница в датах больше 10 лет."
|
||||||
|
f"FTP: {pos_date.date()}, SD: {sd_date.date()}.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.info(f"Найдено расхождение в дате для S/N {rec['serialNumber']} (UUID: {rec['sd_uuid']}). "
|
||||||
|
f"FTP: {pos_date}, SD: {sd_date}. Подготовка к обновлению.")
|
||||||
|
|
||||||
|
# 4. Формируем данные для обновления
|
||||||
|
|
||||||
|
# Проверяем, закончился ли ФН
|
||||||
|
if rec['pos_rnm'] == '0000000000000000':
|
||||||
|
legal_name = 'ЗАКОНЧИЛСЯ ФН'
|
||||||
|
else:
|
||||||
|
legal_name = f"{rec['pos_org_name']} ИНН:{rec['pos_inn']}" if rec['pos_org_name'] and rec['pos_inn'] else rec['pos_org_name']
|
||||||
|
|
||||||
|
# Форматируем даты для API ServiceDesk
|
||||||
|
formatted_expire_date = pos_date.strftime('%Y.%m.%d %H:%M:%S')
|
||||||
|
# Убеждаемся, что дата регистрации тоже есть, прежде чем форматировать
|
||||||
|
formatted_reg_date = parser.parse(rec['pos_date_reg']).strftime('%Y.%m.%d %H:%M:%S') if rec['pos_date_reg'] else None
|
||||||
|
|
||||||
params_to_update = {
|
params_to_update = {
|
||||||
'FNNumber': rec['pos_fn_serial'],
|
'FNNumber': rec['pos_fn_serial'],
|
||||||
'FNExpireDate': parser.parse(rec['pos_date']).strftime('%Y.%m.%d %H:%M:%S'),
|
'FNExpireDate': formatted_expire_date,
|
||||||
'KKTRegDate': parser.parse(rec['pos_reg_date']).strftime('%Y.%m.%d %H:%M:%S'),
|
|
||||||
'LegalName': legal_name,
|
'LegalName': legal_name,
|
||||||
'RNKKT': rec['pos_rnm'],
|
'RNKKT': rec['pos_rnm'],
|
||||||
'FRDownloader': rec['pos_boot_version']
|
'FRDownloader': rec['pos_boot_version'],
|
||||||
|
'KKTRegDate': formatted_reg_date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Удаляем ключи с None-значениями, чтобы не отправлять их в API, если они не обязательны
|
||||||
|
params_to_update = {k: v for k, v in params_to_update.items() if v is not None}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.sd.update_fr(rec['sd_uuid'], params_to_update)
|
self.sd.update_fr(rec['sd_uuid'], params_to_update)
|
||||||
|
update_counter += 1
|
||||||
except ServiceDeskAPIError as e:
|
except ServiceDeskAPIError as e:
|
||||||
log.error(f"Не удалось обновить ФР с UUID {rec['sd_uuid']}: {e}")
|
log.error(f"Не удалось обновить ФР с UUID {rec['sd_uuid']}: {e}")
|
||||||
|
|
||||||
|
except (parser.ParserError, TypeError) as e:
|
||||||
|
log.error(f"Ошибка парсинга даты для S/N {rec['serialNumber']}. "
|
||||||
|
f"FTP_date='{rec['pos_date_end']}', SD_date='{rec['sd_date_end']}'. Ошибка: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.info(f"Проверка обновлений завершена. Обновлено записей: {update_counter}.")
|
||||||
|
|
||||||
|
def _find_lookup_uuid_by_substring(self, lookup_type: str, substring: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Ищет UUID в кэше справочников по подстроке в ключе (title).
|
||||||
|
Например, ищет '13' в ключе '13 мес.'.
|
||||||
|
|
||||||
|
:param lookup_type: Тип справочника ('SrokiFN', 'FFD', etc.).
|
||||||
|
:param substring: Подстрока для поиска (например, '13').
|
||||||
|
:return: UUID или None, если не найдено.
|
||||||
|
"""
|
||||||
|
if not substring or lookup_type not in self.lookup_cache:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Проходим по всем парам (title, uuid) в нужном справочнике
|
||||||
|
for title, uuid in self.lookup_cache[lookup_type].items():
|
||||||
|
# Ищем точное вхождение подстроки, окруженное не-цифрами или границами строки,
|
||||||
|
# чтобы '15' не совпало с '150'.
|
||||||
|
if re.search(r'\b' + re.escape(substring) + r'\b', title):
|
||||||
|
return uuid
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _create_new_frs(self):
|
def _create_new_frs(self):
|
||||||
"""Находит, определяет владельца и создает новые ФР."""
|
"""Находит, определяет владельца и создает новые ФР."""
|
||||||
log.info("Поиск новых ФР для добавления в ServiceDesk...")
|
log.info("Поиск новых ФР для добавления в ServiceDesk...")
|
||||||
@@ -238,10 +304,14 @@ class Synchronizer:
|
|||||||
model_uuid = self.lookup_cache.get('ModeliFR', {}).get(fr['modelName'])
|
model_uuid = self.lookup_cache.get('ModeliFR', {}).get(fr['modelName'])
|
||||||
|
|
||||||
srok_fn_str = _extract_srok_fn_from_execution(fr['fnExecution'])
|
srok_fn_str = _extract_srok_fn_from_execution(fr['fnExecution'])
|
||||||
srok_fn_uuid = self.lookup_cache.get('SrokiFN', {}).get(srok_fn_str) if srok_fn_str else None
|
if not srok_fn_str:
|
||||||
|
srok_fn_str = '13'
|
||||||
|
log.warning(f"Для S/N {fr['serialNumber']} не удалось определить срок ФН из fnExecution. "
|
||||||
|
f"Установлен срок по умолчанию: {srok_fn_str} мес.")
|
||||||
|
srok_fn_uuid = self._find_lookup_uuid_by_substring('SrokiFN', srok_fn_str)
|
||||||
|
|
||||||
ffd_version_str = _map_ffd_version(fr['ffdVersion'])
|
ffd_version_str = _map_ffd_version(fr['ffdVersion'])
|
||||||
ffd_uuid = self.lookup_cache.get('FFD', {}).get(ffd_version_str) if ffd_version_str else None
|
ffd_uuid = self._find_lookup_uuid_by_substring('FFD', ffd_version_str)
|
||||||
|
|
||||||
if not all([model_uuid, srok_fn_uuid, ffd_uuid]):
|
if not all([model_uuid, srok_fn_uuid, ffd_uuid]):
|
||||||
log.error(f"Не удалось создать ФР с S/N {fr['serialNumber']}: не найдены UUID для справочников. "
|
log.error(f"Не удалось создать ФР с S/N {fr['serialNumber']}: не найдены UUID для справочников. "
|
||||||
@@ -270,20 +340,45 @@ class Synchronizer:
|
|||||||
log.error(f"Не удалось создать ФР с S/N {fr['serialNumber']}: {e}")
|
log.error(f"Не удалось создать ФР с S/N {fr['serialNumber']}: {e}")
|
||||||
|
|
||||||
def _find_owner_uuid(self, anydesk_id: Optional[str], teamviewer_id: Optional[str]) -> str:
|
def _find_owner_uuid(self, anydesk_id: Optional[str], teamviewer_id: Optional[str]) -> str:
|
||||||
"""Ищет владельца по ID. Возвращает UUID владельца или UUID по умолчанию."""
|
"""
|
||||||
|
Ищет владельца по ID. При наличии нескольких совпадений для одного ID,
|
||||||
|
выбирает запись с самой свежей датой lastModifiedDate.
|
||||||
|
Возвращает UUID владельца или UUID по умолчанию.
|
||||||
|
"""
|
||||||
|
|
||||||
def query_owner(id_value, id_type_column):
|
def query_best_owner(id_value: Optional[str], id_type_column: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Запрашивает всех владельцев по ID и возвращает UUID самого "свежего".
|
||||||
|
"""
|
||||||
if not id_value:
|
if not id_value:
|
||||||
return None
|
return None
|
||||||
query = f"SELECT owner_uuid FROM workstations WHERE {id_type_column} = ?"
|
|
||||||
result = self.db._execute_query(query, (id_value,), fetch='one')
|
|
||||||
return result['owner_uuid'] if result else None
|
|
||||||
|
|
||||||
owner_by_anydesk = query_owner(anydesk_id, 'clean_anydesk_id')
|
# Запрашиваем всех кандидатов, сортируя по дате в порядке убывания (самые свежие - первые)
|
||||||
owner_by_teamviewer = query_owner(teamviewer_id, 'clean_teamviewer_id')
|
query = f"""
|
||||||
|
SELECT owner_uuid, lastModifiedDate
|
||||||
|
FROM workstations
|
||||||
|
WHERE {id_type_column} = ?
|
||||||
|
ORDER BY lastModifiedDate DESC
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = self.db._execute_query(query, (id_value,), fetch='all')
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(results) > 1:
|
||||||
|
log.warning(f"Найдено несколько ({len(results)}) рабочих станций с {id_type_column} = {id_value}. "
|
||||||
|
f"Выбрана самая свежая запись от {results[0]['lastModifiedDate']}.")
|
||||||
|
|
||||||
|
# Так как мы отсортировали по DESC, первая запись и есть самая свежая
|
||||||
|
return results[0]['owner_uuid']
|
||||||
|
|
||||||
|
owner_by_anydesk = query_best_owner(anydesk_id, 'clean_anydesk_id')
|
||||||
|
owner_by_teamviewer = query_best_owner(teamviewer_id, 'clean_teamviewer_id')
|
||||||
|
|
||||||
|
# Логика выбора остается прежней, но теперь она работает с "лучшими" кандидатами
|
||||||
if owner_by_anydesk and owner_by_teamviewer and owner_by_anydesk == owner_by_teamviewer:
|
if owner_by_anydesk and owner_by_teamviewer and owner_by_anydesk == owner_by_teamviewer:
|
||||||
log.info(f"Владелец {owner_by_anydesk} найден по обоим ID: Anydesk={anydesk_id}, TV={teamviewer_id}.")
|
log.info(f"Владелец {owner_by_anydesk} уверенно найден по обоим ID: Anydesk={anydesk_id}, TV={teamviewer_id}.")
|
||||||
return owner_by_anydesk
|
return owner_by_anydesk
|
||||||
|
|
||||||
if owner_by_anydesk:
|
if owner_by_anydesk:
|
||||||
@@ -297,3 +392,4 @@ class Synchronizer:
|
|||||||
log.warning(f"Владелец не найден для Anydesk={anydesk_id}, TV={teamviewer_id}. "
|
log.warning(f"Владелец не найден для Anydesk={anydesk_id}, TV={teamviewer_id}. "
|
||||||
f"Будет назначен владелец по умолчанию: {config.DEFAULT_OWNER_UUID}")
|
f"Будет назначен владелец по умолчанию: {config.DEFAULT_OWNER_UUID}")
|
||||||
return config.DEFAULT_OWNER_UUID
|
return config.DEFAULT_OWNER_UUID
|
||||||
|
|
||||||
Reference in New Issue
Block a user