full peresobral
This commit is contained in:
316
app.py
316
app.py
@@ -1,316 +0,0 @@
|
|||||||
from scripts import req_all_from_sd as req
|
|
||||||
from scripts import req_by_uuid as get
|
|
||||||
import models
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def fill_companies():
|
|
||||||
metaclass = 'ou$company'
|
|
||||||
attrs = 'adress,title,UUID,KEsInUse,additionalName,childOUs, parent,lastModifiedDate'
|
|
||||||
response = req(metaclass, attrs)
|
|
||||||
if response:
|
|
||||||
with models.session_instance() as session:
|
|
||||||
for company_json in response:
|
|
||||||
existing_company = session.query(models.Company).filter_by(uuid=company_json['UUID']).first()
|
|
||||||
date_string = company_json['lastModifiedDate']
|
|
||||||
strfdate = datetime.strptime(date_string, '%Y.%m.%d %H:%M:%S')
|
|
||||||
base_date = strfdate.strftime('%Y-%m-%dT%H:%M:%S')
|
|
||||||
|
|
||||||
if not existing_company or existing_company.lastmodify.strftime('%Y-%m-%dT%H:%M:%S') != base_date:
|
|
||||||
company = models.Company(
|
|
||||||
title=company_json['title'],
|
|
||||||
uuid=company_json['UUID'],
|
|
||||||
address=company_json['adress'],
|
|
||||||
additional_name=company_json['additionalName'],
|
|
||||||
lastmodify=base_date
|
|
||||||
)
|
|
||||||
kes_in_use = company_json['KEsInUse']
|
|
||||||
if kes_in_use:
|
|
||||||
|
|
||||||
for kes in kes_in_use:
|
|
||||||
if kes['metaClass'] == 'objectBase$Server':
|
|
||||||
|
|
||||||
server = models.Server(
|
|
||||||
uuid = kes['UUID'],
|
|
||||||
owner = company.uuid
|
|
||||||
)
|
|
||||||
company.servers.append(server)
|
|
||||||
if kes['metaClass'] == 'objectBase$Workstation':
|
|
||||||
workstation = models.Workstation(
|
|
||||||
uuid = kes['UUID'],
|
|
||||||
owner = company.uuid
|
|
||||||
)
|
|
||||||
company.workstations.append(workstation)
|
|
||||||
if kes['metaClass'] == 'objectBase$FR':
|
|
||||||
fiscal = models.Fiscalnik(
|
|
||||||
uuid = kes['UUID'],
|
|
||||||
owner = company.uuid
|
|
||||||
)
|
|
||||||
company.fiscals.append(fiscal)
|
|
||||||
|
|
||||||
session.merge(company)
|
|
||||||
else: continue
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def fill_servers():
|
|
||||||
metaclass = 'objectBase$Server'
|
|
||||||
attrs = 'UniqueID,UUID,Teamviewer,AnyDesk,RDP,IP,CabinetLink,DeviceName,owner'
|
|
||||||
response = req(metaclass, attrs)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
servers_json = json.loads(response)
|
|
||||||
for server_json in servers_json:
|
|
||||||
existing_server = session_instance.query(Server).filter_by(uuid=server_json['UUID']).first()
|
|
||||||
if existing_server:
|
|
||||||
existing_server.Teamviewer = server_json.get('Teamviewer') if 'Teamviewer' in server_json else None
|
|
||||||
existing_server.UniqueID = server_json.get('UniqueID') if 'UniqueID' in server_json else None
|
|
||||||
existing_server.AnyDesk = server_json.get('AnyDesk') if 'AnyDesk' in server_json else None
|
|
||||||
existing_server.rdp = server_json.get('RDP') if 'RDP' in server_json else None
|
|
||||||
existing_server.ip = server_json.get('IP') if 'IP' in server_json else None
|
|
||||||
existing_server.CabinetLink = server_json.get('CabinetLink') if 'CabinetLink' in server_json else None
|
|
||||||
existing_server.DeviceName = server_json.get('DeviceName') if 'DeviceName' in server_json else None
|
|
||||||
if 'owner' in server_json and server_json['owner']:
|
|
||||||
owner_uuid = server_json['owner']['UUID']
|
|
||||||
if owner_uuid:
|
|
||||||
company = session_instance.query(Company).filter_by(uuid=owner_uuid).first()
|
|
||||||
if company:
|
|
||||||
existing_server.owner = company
|
|
||||||
session_instance.add(existing_server)
|
|
||||||
session_instance.refresh(existing_server)
|
|
||||||
else:
|
|
||||||
server = Server(
|
|
||||||
uuid=server_json['UUID'],
|
|
||||||
UniqueID=server_json['UniqueID'],
|
|
||||||
Teamviewer=server_json['Teamviewer'] if 'Teamviewer' in server_json else None,
|
|
||||||
AnyDesk=server_json['AnyDesk'] if 'AnyDesk' in server_json else None,
|
|
||||||
rdp=server_json['RDP'] if 'RDP' in server_json else None,
|
|
||||||
ip=server_json['IP'] if 'IP' in server_json else None,
|
|
||||||
CabinetLink=server_json['CabinetLink'] if 'CabinetLink' in server_json else None,
|
|
||||||
DeviceName=server_json['DeviceName'] if 'DeviceName' in server_json else None
|
|
||||||
)
|
|
||||||
if 'owner' in server_json and server_json['owner']:
|
|
||||||
owner_uuid = server_json['owner']['UUID']
|
|
||||||
if owner_uuid:
|
|
||||||
company = session_instance.query(Company).filter_by(uuid=owner_uuid).first()
|
|
||||||
if company:
|
|
||||||
server.owner = company
|
|
||||||
try:
|
|
||||||
session_instance.add(server)
|
|
||||||
session_instance.flush()
|
|
||||||
except:
|
|
||||||
session_instance.rollback()
|
|
||||||
print(server_json['UniqueID'])
|
|
||||||
continue
|
|
||||||
|
|
||||||
session_instance.commit()
|
|
||||||
|
|
||||||
def fill_workstations():
|
|
||||||
metaclass = 'objectBase$Workstation'
|
|
||||||
attrs = 'UUID,AnyDesk,Teamviewer,DeviceName,owner'
|
|
||||||
response = req(metaclass, attrs)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
workstations_json = json.loads(response)
|
|
||||||
for workstation_json in workstations_json:
|
|
||||||
existing_workstation = session_instance.query(Workstation).filter_by(uuid=workstation_json['UUID']).first()
|
|
||||||
if existing_workstation:
|
|
||||||
existing_workstation.AnyDesk = workstation_json.get('AnyDesk') if 'AnyDesk' in workstation_json else None
|
|
||||||
existing_workstation.Teamviewer = workstation_json.get('Teamviewer') if 'Teamviewer' in workstation_json else None
|
|
||||||
existing_workstation.DeviceName = workstation_json.get('DeviceName') if 'DeviceName' in workstation_json else None
|
|
||||||
if 'owner' in workstation_json and workstation_json['owner']:
|
|
||||||
owner_uuid = workstation_json['owner']['UUID']
|
|
||||||
if owner_uuid:
|
|
||||||
company = session_instance.query(Company).filter_by(uuid=owner_uuid).first()
|
|
||||||
if company:
|
|
||||||
existing_workstation.owner = company
|
|
||||||
session_instance.add(existing_workstation)
|
|
||||||
session_instance.refresh(existing_workstation)
|
|
||||||
else:
|
|
||||||
workstation = Workstation(
|
|
||||||
uuid = workstation_json['UUID'],
|
|
||||||
AnyDesk = workstation_json['AnyDesk'] if 'AnyDesk' in workstation_json else None,
|
|
||||||
Teamviewer = workstation_json['Teamviewer'] if 'Teamviewer' in workstation_json else None,
|
|
||||||
DeviceName = workstation_json['DeviceName'] if 'DeviceName' in workstation_json else None
|
|
||||||
)
|
|
||||||
if 'owner' in workstation_json and workstation_json['owner']:
|
|
||||||
owner_uuid = workstation_json['owner']['UUID']
|
|
||||||
if owner_uuid:
|
|
||||||
company = session_instance.query(Company).filter_by(uuid=owner_uuid).first()
|
|
||||||
if company:
|
|
||||||
workstation.owner = company
|
|
||||||
try:
|
|
||||||
session_instance.add(workstation)
|
|
||||||
session_instance.flush()
|
|
||||||
except:
|
|
||||||
session_instance.rollback()
|
|
||||||
print(workstation_json['DeviceName'])
|
|
||||||
continue
|
|
||||||
session_instance.commit()
|
|
||||||
|
|
||||||
def fill_fiscals():
|
|
||||||
metaclass = 'objectBase$FR'
|
|
||||||
attrs = 'RNKKT,KKTRegDate,OFDName,UUID,FNExpireDate,LegalName,FRSerialNumber,ModelKKT,SrokFN,FNNumber,owner'
|
|
||||||
response = req(metaclass, attrs)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
fiscals_json = json.loads(response)
|
|
||||||
for fiscal_json in fiscals_json:
|
|
||||||
existing_fiscal = session_instance.query(Fiscalnik).filter_by(uuid=fiscal_json['UUID']).first()
|
|
||||||
if existing_fiscal:
|
|
||||||
existing_fiscal.rnkkt = fiscal_json.get('RNKKT')
|
|
||||||
existing_fiscal.KKTRegDate = fiscal_json.get('KKTRegDate')
|
|
||||||
existing_fiscal.FNExpireDate = fiscal_json.get('FNExpireDate')
|
|
||||||
existing_fiscal.LegalName = fiscal_json.get('LegalName')
|
|
||||||
existing_fiscal.FRSerialNumber = fiscal_json.get('FRSerialNumber')
|
|
||||||
existing_fiscal.FNNumber = fiscal_json.get('FNNumber')
|
|
||||||
if 'owner' in fiscal_json and fiscal_json['owner']:
|
|
||||||
owner_uuid = fiscal_json['owner']['UUID']
|
|
||||||
if owner_uuid:
|
|
||||||
company = session_instance.query(Company).filter_by(uuid=owner_uuid).first()
|
|
||||||
if company:
|
|
||||||
existing_fiscal.owner = company
|
|
||||||
session_instance.add(existing_fiscal)
|
|
||||||
if 'OFDName' in fiscal_json and fiscal_json['OFDName'] is not None:
|
|
||||||
ofd_name_uuid = fiscal_json['OFDName']['UUID']
|
|
||||||
if ofd_name_uuid:
|
|
||||||
ofd_name = session_instance.query(OFDName).filter_by(uuid=ofd_name_uuid).first()
|
|
||||||
if ofd_name:
|
|
||||||
existing_fiscal.ofd = ofd_name
|
|
||||||
model_kkt_uuid = fiscal_json['ModelKKT']['UUID']
|
|
||||||
if model_kkt_uuid:
|
|
||||||
model_kkt = session_instance.query(ModelKKT).filter_by(uuid=model_kkt_uuid).first()
|
|
||||||
if model_kkt:
|
|
||||||
existing_fiscal.model_kkt = model_kkt
|
|
||||||
srok_fn_uuid = fiscal_json['SrokFN']['UUID']
|
|
||||||
if srok_fn_uuid:
|
|
||||||
srok_fn = session_instance.query(SrokFN).filter_by(uuid=srok_fn_uuid).first()
|
|
||||||
if srok_fn:
|
|
||||||
existing_fiscal.srok_fn = srok_fn
|
|
||||||
session_instance.refresh(existing_fiscal)
|
|
||||||
else:
|
|
||||||
fiscal = Fiscalnik(
|
|
||||||
uuid=fiscal_json['UUID'],
|
|
||||||
rnkkt=fiscal_json['RNKKT'],
|
|
||||||
KKTRegDate=fiscal_json['KKTRegDate'],
|
|
||||||
FNExpireDate=fiscal_json['FNExpireDate'],
|
|
||||||
LegalName=fiscal_json['LegalName'],
|
|
||||||
FRSerialNumber=fiscal_json['FRSerialNumber'],
|
|
||||||
FNNumber=fiscal_json['FNNumber']
|
|
||||||
)
|
|
||||||
if 'owner' in fiscal_json and fiscal_json['owner']:
|
|
||||||
owner_uuid = fiscal_json['owner']['UUID']
|
|
||||||
if owner_uuid:
|
|
||||||
company = session_instance.query(Company).filter_by(uuid=owner_uuid).first()
|
|
||||||
if company:
|
|
||||||
fiscal.owner = company
|
|
||||||
if 'OFDName' in fiscal_json and fiscal_json['OFDName'] is not None:
|
|
||||||
ofd_name_uuid = fiscal_json['OFDName']['UUID']
|
|
||||||
if ofd_name_uuid:
|
|
||||||
ofd_name = session_instance.query(OFDName).filter_by(uuid=ofd_name_uuid).first()
|
|
||||||
if ofd_name:
|
|
||||||
fiscal.ofd = ofd_name
|
|
||||||
model_kkt_uuid = fiscal_json['ModelKKT']['UUID']
|
|
||||||
if model_kkt_uuid:
|
|
||||||
model_kkt = session_instance.query(ModelKKT).filter_by(uuid=model_kkt_uuid).first()
|
|
||||||
if model_kkt:
|
|
||||||
fiscal.model_kkt = model_kkt
|
|
||||||
srok_fn_uuid = fiscal_json['SrokFN']['UUID']
|
|
||||||
if srok_fn_uuid:
|
|
||||||
srok_fn = session_instance.query(SrokFN).filter_by(uuid=srok_fn_uuid).first()
|
|
||||||
if srok_fn:
|
|
||||||
fiscal.srok_fn = srok_fn
|
|
||||||
try:
|
|
||||||
session_instance.add(fiscal)
|
|
||||||
session_instance.flush()
|
|
||||||
except:
|
|
||||||
session_instance.rollback()
|
|
||||||
print(fiscal_json['LegalName'])
|
|
||||||
continue
|
|
||||||
session_instance.commit()
|
|
||||||
|
|
||||||
def fill_ofd_names():
|
|
||||||
metaclass = 'OFD'
|
|
||||||
attrs = 'title,code,UUID'
|
|
||||||
response = req(metaclass, attrs)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
ofd_names_json = json.loads(response)
|
|
||||||
for ofd_name_json in ofd_names_json:
|
|
||||||
ofd_name = OFDName(
|
|
||||||
uuid=ofd_name_json['UUID'],
|
|
||||||
title=ofd_name_json['title'],
|
|
||||||
code=ofd_name_json['code']
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
session_instance.add(ofd_name)
|
|
||||||
session_instance.flush()
|
|
||||||
except:
|
|
||||||
session_instance.rollback()
|
|
||||||
print(ofd_name_json['title'])
|
|
||||||
continue
|
|
||||||
|
|
||||||
session_instance.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def fill_model_kkts():
|
|
||||||
metaclass = 'ModeliFR'
|
|
||||||
attrs = 'title,code,UUID'
|
|
||||||
response = req(metaclass, attrs)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
model_kkts_json = json.loads(response)
|
|
||||||
for model_kkt_json in model_kkts_json:
|
|
||||||
model_kkt = ModelKKT(
|
|
||||||
uuid=model_kkt_json['UUID'],
|
|
||||||
title=model_kkt_json['title'],
|
|
||||||
code=model_kkt_json['code']
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
session_instance.add(model_kkt)
|
|
||||||
session_instance.flush()
|
|
||||||
except:
|
|
||||||
session_instance.rollback()
|
|
||||||
print(model_kkt_json['title'])
|
|
||||||
continue
|
|
||||||
|
|
||||||
session_instance.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def fill_sroki_fns():
|
|
||||||
metaclass = 'SrokiFN'
|
|
||||||
attrs = 'title,code,UUID'
|
|
||||||
response = req(metaclass, attrs)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
sroki_fns_json = json.loads(response)
|
|
||||||
for srok_fn_json in sroki_fns_json:
|
|
||||||
srok_fn = SrokFN(
|
|
||||||
uuid=srok_fn_json['UUID'],
|
|
||||||
title=srok_fn_json['title'],
|
|
||||||
code=srok_fn_json['code']
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
session_instance.add(srok_fn)
|
|
||||||
session_instance.flush()
|
|
||||||
except:
|
|
||||||
session_instance.rollback()
|
|
||||||
print(srok_fn_json['title'])
|
|
||||||
continue
|
|
||||||
|
|
||||||
session_instance.commit()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
fill_companies()
|
|
||||||
# fill_servers()
|
|
||||||
# fill_workstations()
|
|
||||||
# fill_ofd_names()
|
|
||||||
# fill_model_kkts()
|
|
||||||
# fill_sroki_fns()
|
|
||||||
# fill_fiscals()
|
|
||||||
# with session_instance as session:
|
|
||||||
|
|
||||||
# stmt = select(Fiscalnik).where(Fiscalnik.uuid == 'objectBase$5611391')
|
|
||||||
# user = session.scalars(stmt).first()
|
|
||||||
# print(user.model_kkt)
|
|
||||||
34
config.py
Normal file
34
config.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Загружаем переменные из .env файла в окружение
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# --- ServiceDesk API Configuration ---
|
||||||
|
SD_ACCESS_KEY = os.getenv("SDKEY")
|
||||||
|
SD_BASE_URL = os.getenv("SD_BASE_URL")
|
||||||
|
if not all([SD_ACCESS_KEY, SD_BASE_URL]):
|
||||||
|
raise ValueError("Необходимо задать переменные окружения SDKEY и SD_BASE_URL")
|
||||||
|
|
||||||
|
# --- Paths ---
|
||||||
|
DB_PATH = os.getenv("BDPATH", ".") # По умолчанию - текущая папка
|
||||||
|
DB_NAME = "fiscals.db"
|
||||||
|
DB_FULL_PATH = os.path.join(DB_PATH, DB_NAME)
|
||||||
|
JSON_FILES_PATH = os.getenv("JSONPATH")
|
||||||
|
if not JSON_FILES_PATH:
|
||||||
|
raise ValueError("Необходимо задать переменную окружения JSONPATH")
|
||||||
|
|
||||||
|
# --- Sync Logic Configuration ---
|
||||||
|
DEFAULT_OWNER_UUID = os.getenv("DEFAULT_OWNER_UUID")
|
||||||
|
if not DEFAULT_OWNER_UUID:
|
||||||
|
raise ValueError("Необходимо задать UUID владельца по умолчанию в DEFAULT_OWNER_UUID")
|
||||||
|
|
||||||
|
# --- URLs for SD API ---
|
||||||
|
# Используем f-строки для удобного формирования URL
|
||||||
|
FIND_FRS_URL = f"{SD_BASE_URL}/find/objectBase$FR"
|
||||||
|
FIND_WORKSTATIONS_URL = f"{SD_BASE_URL}/find/objectBase$Workstation"
|
||||||
|
# URL для создания будет формироваться динамически, но базовый шаблон такой:
|
||||||
|
CREATE_FR_URL = f"{SD_BASE_URL}/create-m2m/objectBase$FR"
|
||||||
|
# URL для редактирования будет принимать UUID объекта
|
||||||
|
EDIT_FR_URL_TEMPLATE = f"{SD_BASE_URL}/edit/{{uuid}}"
|
||||||
153
database.py
153
database.py
@@ -0,0 +1,153 @@
|
|||||||
|
# database.py
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Настройка логирования для модуля БД
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DatabaseManager:
|
||||||
|
"""
|
||||||
|
Класс для управления всеми операциями с базой данных SQLite.
|
||||||
|
Обеспечивает централизованное подключение и выполнение запросов.
|
||||||
|
"""
|
||||||
|
def __init__(self, db_path):
|
||||||
|
self.db_path = db_path
|
||||||
|
self.conn = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""Открывает соединение с БД при входе в контекстный менеджер."""
|
||||||
|
try:
|
||||||
|
self.conn = sqlite3.connect(self.db_path)
|
||||||
|
# Устанавливаем row_factory для получения результатов в виде словарей
|
||||||
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
log.info(f"Соединение с БД {self.db_path} установлено.")
|
||||||
|
return self
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
log.error(f"Ошибка подключения к БД: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Закрывает соединение при выходе из контекстного менеджера."""
|
||||||
|
if self.conn:
|
||||||
|
self.conn.close()
|
||||||
|
log.info("Соединение с БД закрыто.")
|
||||||
|
|
||||||
|
def _execute_query(self, query, params=(), fetch=None):
|
||||||
|
"""
|
||||||
|
Приватный метод для выполнения SQL-запросов.
|
||||||
|
Использует параметризацию для предотвращения SQL-инъекций.
|
||||||
|
|
||||||
|
:param query: SQL-запрос.
|
||||||
|
:param params: Кортеж с параметрами для запроса.
|
||||||
|
:param fetch: 'one', 'all' или None для коммита.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
cursor.execute(query, params)
|
||||||
|
if fetch == 'one':
|
||||||
|
return cursor.fetchone()
|
||||||
|
elif fetch == 'all':
|
||||||
|
return cursor.fetchall()
|
||||||
|
else:
|
||||||
|
self.conn.commit()
|
||||||
|
return cursor.lastrowid
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
log.error(f"Ошибка выполнения запроса: {query} | {e}")
|
||||||
|
self.conn.rollback() # Откатываем транзакцию в случае ошибки
|
||||||
|
raise
|
||||||
|
|
||||||
|
def setup_tables(self):
|
||||||
|
"""Создает все необходимые таблицы, если они не существуют."""
|
||||||
|
log.info("Проверка и создание таблиц в БД...")
|
||||||
|
# Таблица для данных с FTP
|
||||||
|
self._execute_query("""
|
||||||
|
CREATE TABLE IF NOT EXISTS pos_fiscals (
|
||||||
|
serialNumber TEXT PRIMARY KEY,
|
||||||
|
modelName TEXT,
|
||||||
|
RNM TEXT,
|
||||||
|
organizationName TEXT,
|
||||||
|
fn_serial TEXT,
|
||||||
|
datetime_reg TEXT,
|
||||||
|
dateTime_end TEXT,
|
||||||
|
ofdName TEXT,
|
||||||
|
bootVersion TEXT,
|
||||||
|
ffdVersion TEXT,
|
||||||
|
fnExecution TEXT,
|
||||||
|
INN TEXT,
|
||||||
|
anydesk_id TEXT,
|
||||||
|
teamviewer_id TEXT,
|
||||||
|
lastModifiedDate TEXT
|
||||||
|
)""")
|
||||||
|
|
||||||
|
# Таблица для данных из ServiceDesk
|
||||||
|
self._execute_query("""
|
||||||
|
CREATE TABLE IF NOT EXISTS sd_fiscals (
|
||||||
|
UUID TEXT PRIMARY KEY,
|
||||||
|
serialNumber TEXT UNIQUE,
|
||||||
|
modelName TEXT,
|
||||||
|
RNM TEXT,
|
||||||
|
organizationName TEXT,
|
||||||
|
fn_serial TEXT,
|
||||||
|
datetime_reg TEXT,
|
||||||
|
dateTime_end TEXT,
|
||||||
|
ofdName TEXT,
|
||||||
|
bootVersion TEXT,
|
||||||
|
ffdVersion TEXT,
|
||||||
|
owner_uuid TEXT,
|
||||||
|
lastModifiedDate TEXT
|
||||||
|
)""")
|
||||||
|
|
||||||
|
# Новая таблица для рабочих станций
|
||||||
|
self._execute_query("""
|
||||||
|
CREATE TABLE IF NOT EXISTS workstations (
|
||||||
|
uuid TEXT PRIMARY KEY,
|
||||||
|
owner_uuid TEXT,
|
||||||
|
clean_anydesk_id TEXT,
|
||||||
|
clean_teamviewer_id TEXT
|
||||||
|
)""")
|
||||||
|
|
||||||
|
# Новая таблица для кэширования UUID справочников
|
||||||
|
self._execute_query("""
|
||||||
|
CREATE TABLE IF NOT EXISTS sd_lookups (
|
||||||
|
lookup_type TEXT, -- 'ModelKKT', 'FFD', etc.
|
||||||
|
title TEXT, -- 'АТОЛ 25Ф', '1.2', etc.
|
||||||
|
uuid TEXT,
|
||||||
|
PRIMARY KEY (lookup_type, title)
|
||||||
|
)""")
|
||||||
|
log.info("Все таблицы успешно созданы/проверены.")
|
||||||
|
|
||||||
|
def clear_tables(self, table_names: list):
|
||||||
|
"""Очищает указанные таблицы перед каждым циклом синхронизации."""
|
||||||
|
log.info(f"Очистка таблиц: {', '.join(table_names)}")
|
||||||
|
for table in table_names:
|
||||||
|
# Проверяем, что имя таблицы "безопасное"
|
||||||
|
if table.isalnum():
|
||||||
|
self._execute_query(f"DELETE FROM {table}")
|
||||||
|
else:
|
||||||
|
log.warning(f"Попытка очистить таблицу с некорректным именем: {table}")
|
||||||
|
|
||||||
|
def bulk_insert(self, table_name: str, columns: list, data: list):
|
||||||
|
"""
|
||||||
|
Выполняет массовую вставку данных в таблицу.
|
||||||
|
|
||||||
|
:param table_name: Имя таблицы.
|
||||||
|
:param columns: Список названий колонок.
|
||||||
|
:param data: Список кортежей с данными для вставки.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
placeholders = ', '.join(['?'] * len(columns))
|
||||||
|
cols = ', '.join(columns)
|
||||||
|
query = f"INSERT OR REPLACE INTO {table_name} ({cols}) VALUES ({placeholders})"
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
cursor.executemany(query, data)
|
||||||
|
self.conn.commit()
|
||||||
|
log.info(f"Вставлено {len(data)} записей в таблицу {table_name}.")
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
log.error(f"Ошибка массовой вставки в {table_name}: {e}")
|
||||||
|
self.conn.rollback()
|
||||||
|
raise
|
||||||
66
ftp_parser.py
Normal file
66
ftp_parser.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# ftp_parser.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def process_json_files(directory: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Сканирует директорию, читает все .json файлы и извлекает из них данные
|
||||||
|
о фискальных регистраторах в стандартизированном формате.
|
||||||
|
|
||||||
|
:param directory: Путь к директории с JSON файлами.
|
||||||
|
:return: Список словарей, где каждый словарь представляет один ФР.
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(directory):
|
||||||
|
log.error(f"Указанный путь не является директорией: {directory}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
all_fr_data = []
|
||||||
|
log.info(f"Начинаю обработку JSON файлов из директории: {directory}")
|
||||||
|
|
||||||
|
for filename in os.listdir(directory):
|
||||||
|
if filename.lower().endswith('.json'):
|
||||||
|
file_path = os.path.join(directory, filename)
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
if 'serialNumber' not in data or not data['serialNumber']:
|
||||||
|
log.warning(f"Пропущен файл {filename}: отсутствует serialNumber.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Поля teamviewer_id могут быть написаны по-разному,
|
||||||
|
# например teamviever_id. Обработаем это.
|
||||||
|
teamviewer_id = data.get('teamviewer_id') or data.get('teamviever_id')
|
||||||
|
|
||||||
|
fr_record = {
|
||||||
|
'serialNumber': data.get('serialNumber'),
|
||||||
|
'modelName': data.get('modelName'),
|
||||||
|
'RNM': data.get('RNM') or '0000000000000000',
|
||||||
|
'organizationName': data.get('organizationName'),
|
||||||
|
'fn_serial': data.get('fn_serial') or '0000000000000000',
|
||||||
|
'datetime_reg': data.get('datetime_reg'),
|
||||||
|
'dateTime_end': data.get('dateTime_end'),
|
||||||
|
'ofdName': data.get('ofdName'),
|
||||||
|
'bootVersion': data.get('bootVersion'),
|
||||||
|
'ffdVersion': data.get('ffdVersion'), # В примере '120', а не '1.2'. Нужно будет уточнить.
|
||||||
|
'fnExecution': data.get('fnExecution'),
|
||||||
|
'INN': data.get('INN'),
|
||||||
|
'anydesk_id': data.get('anydesk_id'), # Приходят чистые
|
||||||
|
'teamviewer_id': teamviewer_id, # Приходят чистые
|
||||||
|
'lastModifiedDate': data.get('current_time')
|
||||||
|
}
|
||||||
|
all_fr_data.append(fr_record)
|
||||||
|
log.debug(f"Успешно обработан файл {filename}. S/N: {fr_record['serialNumber']}")
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
log.error(f"Ошибка декодирования JSON в файле: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Неизвестная ошибка при обработке файла {filename}: {e}")
|
||||||
|
|
||||||
|
log.info(f"Обработка JSON файлов завершена. Получено {len(all_fr_data)} записей.")
|
||||||
|
return all_fr_data
|
||||||
65
main.py
Normal file
65
main.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# main.py
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import schedule
|
||||||
|
|
||||||
|
import config
|
||||||
|
from database import DatabaseManager
|
||||||
|
from sd_api import ServiceDeskClient
|
||||||
|
from sync_logic import Synchronizer
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Настраивает базовую конфигурацию логирования."""
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
stream=sys.stdout, # Вывод логов в stdout
|
||||||
|
)
|
||||||
|
# Отключаем слишком "болтливые" логи от сторонних библиотек
|
||||||
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
def job():
|
||||||
|
"""
|
||||||
|
Основная задача, которую будет выполнять планировщик.
|
||||||
|
"""
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.info("Запуск задачи синхронизации...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Инициализация всех компонентов
|
||||||
|
db_manager = DatabaseManager(config.DB_FULL_PATH)
|
||||||
|
sd_client = ServiceDeskClient(config.SD_ACCESS_KEY, config.SD_BASE_URL)
|
||||||
|
|
||||||
|
# Контекстный менеджер для БД гарантирует открытие/закрытие соединения
|
||||||
|
with db_manager as db:
|
||||||
|
# Перед первым запуском создаем таблицы, если их нет
|
||||||
|
db.setup_tables()
|
||||||
|
|
||||||
|
synchronizer = Synchronizer(db, sd_client)
|
||||||
|
synchronizer.run_full_sync()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.critical(f"Произошла непредвиденная ошибка на верхнем уровне: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
setup_logging()
|
||||||
|
main_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# --- Первоначальный запуск ---
|
||||||
|
# Можно запустить один раз при старте, чтобы не ждать первого интервала
|
||||||
|
main_log.info("Первый запуск сервиса...")
|
||||||
|
job()
|
||||||
|
|
||||||
|
# --- Настройка расписания ---
|
||||||
|
# В вашем коде было 3 минуты, можно настроить как угодно
|
||||||
|
sync_interval_minutes = 15
|
||||||
|
main_log.info(f"Сервис запущен. Синхронизация будет выполняться каждые {sync_interval_minutes} минут.")
|
||||||
|
schedule.every(sync_interval_minutes).minutes.do(job)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(1)
|
||||||
122
models.py
122
models.py
@@ -1,122 +0,0 @@
|
|||||||
from sqlalchemy import create_engine, String, String, ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship, sessionmaker, Mapped, mapped_column, DeclarativeBase
|
|
||||||
from typing import List
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
engine = create_engine('postgresql://postgres:994525@10.25.1.250:5432/mhtest', echo=True)
|
|
||||||
session_instance = sessionmaker(engine)
|
|
||||||
|
|
||||||
|
|
||||||
class Company(Base):
|
|
||||||
__tablename__ = 'companies'
|
|
||||||
|
|
||||||
uuid: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
title: Mapped[str] = mapped_column(String(100))
|
|
||||||
address: Mapped[str | None] = mapped_column(String(250))
|
|
||||||
additional_name: Mapped[str | None] = mapped_column(String(150))
|
|
||||||
lastmodify: Mapped[datetime]
|
|
||||||
# equipinuse: Mapped[List['Server','Workstation','Fiscalnik']] = relationship(back_populates="owner")
|
|
||||||
|
|
||||||
servers: Mapped[List["Server"]] = relationship(back_populates="owner")
|
|
||||||
workstations: Mapped[List["Workstation"]] = relationship(back_populates="owner")
|
|
||||||
fiscals: Mapped[List["Fiscalnik"]] = relationship(back_populates="owner")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.title}'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Server(Base):
|
|
||||||
__tablename__ = 'servers'
|
|
||||||
|
|
||||||
uuid: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
UniqueID: Mapped[str | None] = mapped_column(String(50))
|
|
||||||
Teamviewer: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
AnyDesk: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
rdp: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
ip: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
CabinetLink: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
DeviceName: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
lastmodify: Mapped[datetime]
|
|
||||||
|
|
||||||
company_uuid: Mapped[str] = mapped_column(ForeignKey('companies.uuid'))
|
|
||||||
owner: Mapped["Company"] = relationship(back_populates="servers")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.DeviceName}'
|
|
||||||
|
|
||||||
class Workstation(Base):
|
|
||||||
__tablename__ = 'workstations'
|
|
||||||
|
|
||||||
uuid: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
AnyDesk: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
Teamviewer: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
DeviceName: Mapped[str | None] = mapped_column(String(100))
|
|
||||||
lastmodify: Mapped[datetime]
|
|
||||||
|
|
||||||
company_uuid: Mapped[str] = mapped_column(ForeignKey('companies.uuid'))
|
|
||||||
owner: Mapped["Company"] = relationship(back_populates="workstations")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.DeviceName}'
|
|
||||||
|
|
||||||
class Fiscalnik(Base):
|
|
||||||
__tablename__ = 'fiscals'
|
|
||||||
|
|
||||||
uuid: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
rnkkt: Mapped[str | None] = mapped_column(String(50))
|
|
||||||
LegalName: Mapped[str] = mapped_column(String(150))
|
|
||||||
FNNumber: Mapped[int]
|
|
||||||
KKTRegDate: Mapped[datetime]
|
|
||||||
FNExpireDate: Mapped[datetime]
|
|
||||||
FRSerialNumber: Mapped[int]
|
|
||||||
lastmodify: Mapped[datetime]
|
|
||||||
|
|
||||||
company_uuid: Mapped[str] = mapped_column(ForeignKey('companies.uuid'))
|
|
||||||
owner: Mapped["Company"] = relationship(back_populates="fiscals")
|
|
||||||
|
|
||||||
ofd_uuid: Mapped[str] = mapped_column(ForeignKey('ofd_names.uuid'))
|
|
||||||
ofd: Mapped["OFDName"] = relationship()
|
|
||||||
model_kkt_uuid: Mapped[str] = mapped_column(ForeignKey('model_kkts.uuid'))
|
|
||||||
model_kkt: Mapped["ModelKKT"] = relationship()
|
|
||||||
srok_fn_uuid: Mapped[str] = mapped_column(ForeignKey('sroki_fns.uuid'))
|
|
||||||
srok_fn: Mapped["SrokFN"] = relationship()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.FRSerialNumber}'
|
|
||||||
|
|
||||||
class OFDName(Base):
|
|
||||||
__tablename__ = 'ofd_names'
|
|
||||||
|
|
||||||
uuid: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
title: Mapped[str]
|
|
||||||
code: Mapped[str]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.title}'
|
|
||||||
|
|
||||||
class ModelKKT(Base):
|
|
||||||
__tablename__ = 'model_kkts'
|
|
||||||
|
|
||||||
uuid: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
title: Mapped[str]
|
|
||||||
code: Mapped[str]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.title}'
|
|
||||||
|
|
||||||
class SrokFN(Base):
|
|
||||||
__tablename__ = 'sroki_fns'
|
|
||||||
|
|
||||||
uuid: Mapped[str] = mapped_column(primary_key=True)
|
|
||||||
title: Mapped[str]
|
|
||||||
code: Mapped[str]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.title}'
|
|
||||||
|
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
File diff suppressed because one or more lines are too long
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
python-dotenv
|
||||||
|
requests
|
||||||
|
schedule
|
||||||
|
python-dateutil
|
||||||
45
scripts.py
45
scripts.py
@@ -1,45 +0,0 @@
|
|||||||
import mureq, os, sys, re
|
|
||||||
|
|
||||||
|
|
||||||
def req_all_from_sd(metaclass, attrs) -> dict | Exception:
|
|
||||||
url = f'https://myhoreca.itsm365.com/sd/services/rest/find/{metaclass}'
|
|
||||||
access_key = os.getenv('SDKEY')
|
|
||||||
if access_key is None:
|
|
||||||
sys.stderr.write("Ошибка: Не удалось получить доступный ключ для запроса\n")
|
|
||||||
return Exception
|
|
||||||
response = mureq.get(url, params={'accessKey': access_key, 'attrs': attrs})
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def req_by_uuid(metaclass, uuid) -> dict | Exception:
|
|
||||||
url = f'https://myhoreca.itsm365.com/sd/services/rest/get/{uuid}'
|
|
||||||
access_key = os.getenv('SDKEY')
|
|
||||||
if access_key is None:
|
|
||||||
sys.stderr.write("Ошибка: Не удалось получить доступный ключ для запроса\n")
|
|
||||||
return Exception
|
|
||||||
|
|
||||||
match metaclass:
|
|
||||||
case 'objectBase$Workstation':
|
|
||||||
attrs = 'UUID,AnyDesk,Teamviewer,DeviceName,owner,lastModifiedDate'
|
|
||||||
case 'ou$company':
|
|
||||||
attrs = 'adress,title,UUID,KEsInUse,additionalName,childOUs,parent,lastModifiedDate'
|
|
||||||
case 'objectBase$Server':
|
|
||||||
attrs = 'UniqueID,UUID,Teamviewer,AnyDesk,RDP,IP,CabinetLink,DeviceName,owner,lastModifiedDate'
|
|
||||||
case 'objectBase$FR':
|
|
||||||
attrs = 'RNKKT,KKTRegDate,OFDName,UUID,FNExpireDate,LegalName,FRSerialNumber,ModelKKT,SrokFN,FNNumber,owner,lastModifiedDate'
|
|
||||||
case _:
|
|
||||||
attrs = False
|
|
||||||
|
|
||||||
if attrs: response = mureq.get(url, params={'accessKey': access_key, 'attrs': attrs})
|
|
||||||
else: response = mureq.get(url, params={'accessKey': access_key})
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()
|
|
||||||
else:
|
|
||||||
return Exception
|
|
||||||
|
|
||||||
|
|
||||||
companys = req_by_uuid('objectBase$Workstation', 'objectBase$8203200')
|
|
||||||
print(companys)
|
|
||||||
121
sd_api.py
Normal file
121
sd_api.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# sd_api.py
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
# Импортируем нашу конфигурацию
|
||||||
|
import config
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ServiceDeskAPIError(Exception):
|
||||||
|
"""Кастомное исключение для ошибок API ServiceDesk."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ServiceDeskClient:
|
||||||
|
"""
|
||||||
|
Клиент для взаимодействия с REST API ServiceDesk.
|
||||||
|
"""
|
||||||
|
def __init__(self, access_key: str, base_url: str):
|
||||||
|
if not access_key or not base_url:
|
||||||
|
raise ValueError("Access key and base URL cannot be empty.")
|
||||||
|
|
||||||
|
self.base_url = base_url
|
||||||
|
self.session = requests.Session()
|
||||||
|
# Устанавливаем accessKey как параметр по умолчанию для всех запросов
|
||||||
|
self.session.params = {'accessKey': access_key}
|
||||||
|
|
||||||
|
def _make_request(self, method: str, url: str, params: Dict = None, json_data: Dict = None) -> Any:
|
||||||
|
"""
|
||||||
|
Внутренний метод для выполнения HTTP-запросов.
|
||||||
|
|
||||||
|
:param method: HTTP метод ('GET', 'POST', etc.)
|
||||||
|
:param url: Полный URL для запроса.
|
||||||
|
:param params: Дополнительные параметры URL (кроме accessKey).
|
||||||
|
:param json_data: Тело запроса в формате JSON для POST/PUT.
|
||||||
|
:return: Ответ сервера в виде JSON.
|
||||||
|
:raises ServiceDeskAPIError: в случае ошибки API или сети.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.session.request(method, url, params=params, json=json_data, timeout=30)
|
||||||
|
response.raise_for_status() # Вызовет исключение для кодов 4xx/5xx
|
||||||
|
|
||||||
|
# Некоторые ответы могут быть пустыми (например, при успешном редактировании)
|
||||||
|
if response.status_code == 204 or not response.text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
error_message = f"HTTP Error: {e.response.status_code} for URL {url}. Response: {e.response.text}"
|
||||||
|
log.error(error_message)
|
||||||
|
raise ServiceDeskAPIError(error_message) from e
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
error_message = f"Request failed for URL {url}: {e}"
|
||||||
|
log.error(error_message)
|
||||||
|
raise ServiceDeskAPIError(error_message) from e
|
||||||
|
|
||||||
|
def get_all_frs(self) -> List[Dict]:
|
||||||
|
"""Получает список всех фискальных регистраторов."""
|
||||||
|
log.info("Запрос списка всех ФР из ServiceDesk...")
|
||||||
|
params = {
|
||||||
|
'attrs': 'UUID,FRSerialNumber,RNKKT,KKTRegDate,FNExpireDate,FNNumber,owner,FRDownloader,LegalName,OFDName,ModelKKT,FFD,lastModifiedDate'
|
||||||
|
}
|
||||||
|
frs = self._make_request('POST', config.FIND_FRS_URL, params=params)
|
||||||
|
log.info(f"Получено {len(frs)} записей о ФР.")
|
||||||
|
return frs
|
||||||
|
|
||||||
|
def get_all_workstations(self) -> List[Dict]:
|
||||||
|
"""Получает список всех рабочих станций."""
|
||||||
|
log.info("Запрос списка всех рабочих станций из ServiceDesk...")
|
||||||
|
params = {
|
||||||
|
'attrs': 'UUID,owner,AnyDesk,Teamviewer'
|
||||||
|
}
|
||||||
|
workstations = self._make_request('POST', config.FIND_WORKSTATIONS_URL, params=params)
|
||||||
|
log.info(f"Получено {len(workstations)} записей о рабочих станциях.")
|
||||||
|
return workstations
|
||||||
|
|
||||||
|
def get_lookup_values(self, metaclass: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Получает значения из справочника по его metaClass.
|
||||||
|
|
||||||
|
:param metaclass: metaClass справочника (e.g., 'ModeliFR', 'FFD').
|
||||||
|
:return: Список словарей с 'UUID' и 'title'.
|
||||||
|
"""
|
||||||
|
log.info(f"Запрос значений справочника для metaClass: {metaclass}...")
|
||||||
|
url = f"{self.base_url}/find/{metaclass}"
|
||||||
|
params = {'attrs': 'UUID,title'}
|
||||||
|
lookups = self._make_request('POST', url, params=params)
|
||||||
|
log.info(f"Получено {len(lookups)} значений для {metaclass}.")
|
||||||
|
return lookups
|
||||||
|
|
||||||
|
def update_fr(self, uuid: str, data: Dict) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет существующий фискальный регистратор.
|
||||||
|
|
||||||
|
:param uuid: UUID объекта для редактирования.
|
||||||
|
:param data: Словарь с полями для обновления.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
self._make_request('POST', url, params=data)
|
||||||
|
log.info(f"Объект {uuid} успешно обновлен.")
|
||||||
|
|
||||||
|
def create_fr(self, data: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Создает новый фискальный регистратор.
|
||||||
|
|
||||||
|
:param data: Словарь с данными для создания объекта (тело запроса).
|
||||||
|
:return: JSON-ответ от сервера, содержащий UUID нового объекта.
|
||||||
|
"""
|
||||||
|
log.info(f"Создание нового ФР с серийным номером: {data.get('FRSerialNumber')}...")
|
||||||
|
# Параметр для получения UUID в ответе
|
||||||
|
params = {'attrs': 'UUID'}
|
||||||
|
response = self._make_request('POST', config.CREATE_FR_URL, params=params, json_data=data)
|
||||||
|
log.info(f"ФР успешно создан. Ответ сервера: {response}")
|
||||||
|
return response
|
||||||
299
sync_logic.py
Normal file
299
sync_logic.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# sync_logic.py
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from dateutil import parser
|
||||||
|
|
||||||
|
# Импортируем наши модули
|
||||||
|
import config
|
||||||
|
from database import DatabaseManager
|
||||||
|
from sd_api import ServiceDeskClient, ServiceDeskAPIError
|
||||||
|
import ftp_parser
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_sd_remote_id(raw_id: Optional[str]) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Очищает ID удаленного доступа, полученный из ServiceDesk.
|
||||||
|
(Эта функция была ранее в ftp_parser, но перенесена сюда,
|
||||||
|
так как "грязные" ID приходят из SD).
|
||||||
|
"""
|
||||||
|
if not raw_id or raw_id.lower() == 'none':
|
||||||
|
return None
|
||||||
|
no_spaces_id = re.sub(r'\s+', '', raw_id)
|
||||||
|
match = re.search(r'\d+', no_spaces_id)
|
||||||
|
return match.group(0) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_srok_fn_from_execution(fn_execution: Optional[str]) -> Optional[str]:
|
||||||
|
"""Извлекает срок ФН (13, 15, 36) из строки fnExecution."""
|
||||||
|
if not fn_execution:
|
||||||
|
return None
|
||||||
|
# Ищем числа 13, 15 или 36 в строке
|
||||||
|
match = re.search(r'(13|15|36)', fn_execution)
|
||||||
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def _map_ffd_version(ffd_from_ftp: Optional[str]) -> Optional[str]:
|
||||||
|
"""Сопоставляет версию ФФД из FTP ('120') с версией в SD ('1.2')."""
|
||||||
|
if ffd_from_ftp == '120':
|
||||||
|
return '1.2'
|
||||||
|
if ffd_from_ftp == '105':
|
||||||
|
return '1.05'
|
||||||
|
# Можно добавить другие сопоставления, если появятся
|
||||||
|
return ffd_from_ftp # Возвращаем как есть, если нет сопоставления
|
||||||
|
|
||||||
|
|
||||||
|
class Synchronizer:
|
||||||
|
"""
|
||||||
|
Класс, инкапсулирующий всю логику синхронизации данных.
|
||||||
|
"""
|
||||||
|
def __init__(self, db_manager: DatabaseManager, sd_client: ServiceDeskClient):
|
||||||
|
self.db = db_manager
|
||||||
|
self.sd = sd_client
|
||||||
|
self.lookup_cache: Dict[str, Dict[str, str]] = {}
|
||||||
|
|
||||||
|
def _prepare_data_for_db(self, fr_list: List[Dict], source: str) -> List[tuple]:
|
||||||
|
"""
|
||||||
|
Преобразует список словарей в список кортежей для массовой вставки в БД.
|
||||||
|
Также логирует отсутствующие, но важные поля.
|
||||||
|
"""
|
||||||
|
prepared_data = []
|
||||||
|
|
||||||
|
if source == 'pos':
|
||||||
|
# Ожидаемые колонки для pos_fiscals
|
||||||
|
columns = ('serialNumber', 'modelName', 'RNM', 'organizationName', 'fn_serial',
|
||||||
|
'datetime_reg', 'dateTime_end', 'ofdName', 'bootVersion', 'ffdVersion',
|
||||||
|
'fnExecution', 'INN', 'anydesk_id', 'teamviewer_id', 'lastModifiedDate')
|
||||||
|
|
||||||
|
for fr in fr_list:
|
||||||
|
# Проверка на наличие обязательных полей
|
||||||
|
if not all(fr.get(key) for key in ['serialNumber', 'modelName', 'dateTime_end']):
|
||||||
|
log.warning(f"Пропущена запись из FTP: отсутствует одно из ключевых полей "
|
||||||
|
f"(serialNumber, modelName, dateTime_end). Данные: {fr}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
prepared_data.append(tuple(fr.get(col) for col in columns))
|
||||||
|
|
||||||
|
elif source == 'sd':
|
||||||
|
# Ожидаемые колонки для sd_fiscals
|
||||||
|
columns = ('UUID', 'serialNumber', 'modelName', 'RNM', 'organizationName', 'fn_serial',
|
||||||
|
'datetime_reg', 'dateTime_end', 'ofdName', 'bootVersion', 'ffdVersion',
|
||||||
|
'owner_uuid', 'lastModifiedDate')
|
||||||
|
|
||||||
|
for fr in fr_list:
|
||||||
|
if not fr.get('UUID') or not fr.get('FRSerialNumber'):
|
||||||
|
log.warning(f"Пропущена запись из SD: отсутствует UUID или FRSerialNumber. Данные: {fr}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Извлекаем и нормализуем данные
|
||||||
|
owner = fr.get('owner')
|
||||||
|
model = fr.get('ModelKKT')
|
||||||
|
ofd = fr.get('OFDName')
|
||||||
|
ffd = fr.get('FFD')
|
||||||
|
|
||||||
|
prepared_data.append((
|
||||||
|
fr.get('UUID'),
|
||||||
|
fr.get('FRSerialNumber'),
|
||||||
|
model.get('title') if model else None,
|
||||||
|
fr.get('RNKKT'),
|
||||||
|
fr.get('LegalName'),
|
||||||
|
fr.get('FNNumber'),
|
||||||
|
fr.get('KKTRegDate'),
|
||||||
|
fr.get('FNExpireDate'),
|
||||||
|
ofd.get('title') if ofd else None,
|
||||||
|
fr.get('FRDownloader'),
|
||||||
|
ffd.get('title') if ffd else None,
|
||||||
|
owner.get('UUID') if owner else None,
|
||||||
|
fr.get('lastModifiedDate')
|
||||||
|
))
|
||||||
|
return prepared_data, columns
|
||||||
|
|
||||||
|
def _fetch_and_cache_lookups(self):
|
||||||
|
"""Запрашивает и кэширует UUID справочников из SD."""
|
||||||
|
log.info("Кэширование справочников из ServiceDesk...")
|
||||||
|
metaclasses = ['ModeliFR', 'SrokiFN', 'FFD']
|
||||||
|
for mc in metaclasses:
|
||||||
|
try:
|
||||||
|
values = self.sd.get_lookup_values(mc)
|
||||||
|
# Создаем словарь вида {'Название': 'UUID'}
|
||||||
|
self.lookup_cache[mc] = {item['title']: item['UUID'] for item in values}
|
||||||
|
except ServiceDeskAPIError as e:
|
||||||
|
log.error(f"Не удалось загрузить справочник {mc}: {e}")
|
||||||
|
# Если не удалось загрузить критически важные справочники, прерываем работу
|
||||||
|
raise
|
||||||
|
log.info("Справочники успешно закэшированы.")
|
||||||
|
|
||||||
|
def run_full_sync(self):
|
||||||
|
"""Выполняет полный цикл синхронизации."""
|
||||||
|
log.info("--- НАЧАЛО ЦИКЛА СИНХРОНИЗАЦИИ ---")
|
||||||
|
try:
|
||||||
|
# 1. Кэшируем справочники
|
||||||
|
self._fetch_and_cache_lookups()
|
||||||
|
|
||||||
|
# 2. Очищаем таблицы перед заполнением
|
||||||
|
self.db.clear_tables(['pos_fiscals', 'sd_fiscals', 'workstations'])
|
||||||
|
|
||||||
|
# 3. Сбор и загрузка данных с FTP
|
||||||
|
pos_frs_raw = ftp_parser.process_json_files(config.JSON_FILES_PATH)
|
||||||
|
pos_frs_data, pos_cols = self._prepare_data_for_db(pos_frs_raw, 'pos')
|
||||||
|
self.db.bulk_insert('pos_fiscals', pos_cols, pos_frs_data)
|
||||||
|
|
||||||
|
# 4. Сбор и загрузка данных из ServiceDesk (ФР)
|
||||||
|
sd_frs_raw = self.sd.get_all_frs()
|
||||||
|
sd_frs_data, sd_cols = self._prepare_data_for_db(sd_frs_raw, 'sd')
|
||||||
|
self.db.bulk_insert('sd_fiscals', sd_cols, sd_frs_data)
|
||||||
|
|
||||||
|
# 5. Сбор и загрузка данных из ServiceDesk (Рабочие станции)
|
||||||
|
workstations_raw = self.sd.get_all_workstations()
|
||||||
|
workstations_data = []
|
||||||
|
for ws in workstations_raw:
|
||||||
|
owner = ws.get('owner')
|
||||||
|
if ws.get('UUID') and owner and owner.get('UUID'):
|
||||||
|
workstations_data.append((
|
||||||
|
ws['UUID'],
|
||||||
|
owner['UUID'],
|
||||||
|
_clean_sd_remote_id(ws.get('AnyDesk')),
|
||||||
|
_clean_sd_remote_id(ws.get('Teamviewer'))
|
||||||
|
))
|
||||||
|
self.db.bulk_insert('workstations', ['uuid', 'owner_uuid', 'clean_anydesk_id', 'clean_teamviewer_id'], workstations_data)
|
||||||
|
|
||||||
|
# 6. Логика синхронизации
|
||||||
|
self._update_existing_frs()
|
||||||
|
self._create_new_frs()
|
||||||
|
|
||||||
|
log.info("--- ЦИКЛ СИНХРОНИЗАЦИИ УСПЕШНО ЗАВЕРШЕН ---")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.critical(f"Критическая ошибка в цикле синхронизации: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_existing_frs(self):
|
||||||
|
"""Находит и обновляет ФР с отличающимися датами."""
|
||||||
|
log.info("Поиск ФР для обновления даты окончания ФН...")
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
pos.serialNumber,
|
||||||
|
pos.dateTime_end AS pos_date,
|
||||||
|
pos.datetime_reg AS pos_reg_date,
|
||||||
|
pos.fn_serial AS pos_fn_serial,
|
||||||
|
pos.RNM AS pos_rnm,
|
||||||
|
pos.bootVersion AS pos_boot_version,
|
||||||
|
pos.organizationName AS pos_org_name,
|
||||||
|
pos.INN AS pos_inn,
|
||||||
|
sd.UUID AS sd_uuid,
|
||||||
|
sd.dateTime_end AS sd_date
|
||||||
|
FROM pos_fiscals pos
|
||||||
|
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')
|
||||||
|
log.info(f"Найдено {len(records_to_update)} ФР для обновления.")
|
||||||
|
|
||||||
|
for rec in records_to_update:
|
||||||
|
# Проверка, что дата действительно новее (избегаем "старых" данных с FTP)
|
||||||
|
# Это простая проверка, можно усложнить, если нужно
|
||||||
|
if parser.parse(rec['pos_date']) < parser.parse(rec['sd_date']):
|
||||||
|
log.warning(f"Пропуск обновления для S/N {rec['serialNumber']}: дата с FTP ({rec['pos_date']}) "
|
||||||
|
f"старше, чем в SD ({rec['sd_date']}).")
|
||||||
|
continue
|
||||||
|
|
||||||
|
legal_name = f"{rec['pos_org_name']} ИНН:{rec['pos_inn']}" if rec['pos_org_name'] and rec['pos_inn'] else "ЗАКОНЧИЛСЯ ФН"
|
||||||
|
|
||||||
|
params_to_update = {
|
||||||
|
'FNNumber': rec['pos_fn_serial'],
|
||||||
|
'FNExpireDate': parser.parse(rec['pos_date']).strftime('%Y.%m.%d %H:%M:%S'),
|
||||||
|
'KKTRegDate': parser.parse(rec['pos_reg_date']).strftime('%Y.%m.%d %H:%M:%S'),
|
||||||
|
'LegalName': legal_name,
|
||||||
|
'RNKKT': rec['pos_rnm'],
|
||||||
|
'FRDownloader': rec['pos_boot_version']
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self.sd.update_fr(rec['sd_uuid'], params_to_update)
|
||||||
|
except ServiceDeskAPIError as e:
|
||||||
|
log.error(f"Не удалось обновить ФР с UUID {rec['sd_uuid']}: {e}")
|
||||||
|
|
||||||
|
def _create_new_frs(self):
|
||||||
|
"""Находит, определяет владельца и создает новые ФР."""
|
||||||
|
log.info("Поиск новых ФР для добавления в ServiceDesk...")
|
||||||
|
query = """
|
||||||
|
SELECT pos.*
|
||||||
|
FROM pos_fiscals pos
|
||||||
|
LEFT JOIN sd_fiscals sd ON pos.serialNumber = sd.serialNumber
|
||||||
|
WHERE sd.serialNumber IS NULL
|
||||||
|
"""
|
||||||
|
new_frs = self.db._execute_query(query, fetch='all')
|
||||||
|
log.info(f"Найдено {len(new_frs)} новых ФР.")
|
||||||
|
|
||||||
|
for fr in new_frs:
|
||||||
|
owner_uuid = self._find_owner_uuid(fr['anydesk_id'], fr['teamviewer_id'])
|
||||||
|
|
||||||
|
# Собираем данные для создания
|
||||||
|
model_uuid = self.lookup_cache.get('ModeliFR', {}).get(fr['modelName'])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if not all([model_uuid, srok_fn_uuid, ffd_uuid]):
|
||||||
|
log.error(f"Не удалось создать ФР с S/N {fr['serialNumber']}: не найдены UUID для справочников. "
|
||||||
|
f"Model: {fr['modelName']}({model_uuid}), SrokFN: {srok_fn_str}({srok_fn_uuid}), "
|
||||||
|
f"FFD: {ffd_version_str}({ffd_uuid})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
legal_name = f"{fr['organizationName']} ИНН:{fr['INN']}" if fr['organizationName'] and fr['INN'] else ""
|
||||||
|
|
||||||
|
creation_data = {
|
||||||
|
"RNKKT": fr['RNM'],
|
||||||
|
"KKTRegDate": parser.parse(fr['datetime_reg']).strftime('%Y.%m.%d %H:%M:%S'),
|
||||||
|
"FRSerialNumber": fr['serialNumber'],
|
||||||
|
"LegalName": legal_name,
|
||||||
|
"FNExpireDate": parser.parse(fr['dateTime_end']).strftime('%Y.%m.%d %H:%M:%S'),
|
||||||
|
"FNNumber": fr['fn_serial'],
|
||||||
|
"ModelKKT": model_uuid,
|
||||||
|
"SrokFN": srok_fn_uuid,
|
||||||
|
"FFD": ffd_uuid,
|
||||||
|
"owner": owner_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sd.create_fr(creation_data)
|
||||||
|
except ServiceDeskAPIError as e:
|
||||||
|
log.error(f"Не удалось создать ФР с S/N {fr['serialNumber']}: {e}")
|
||||||
|
|
||||||
|
def _find_owner_uuid(self, anydesk_id: Optional[str], teamviewer_id: Optional[str]) -> str:
|
||||||
|
"""Ищет владельца по ID. Возвращает UUID владельца или UUID по умолчанию."""
|
||||||
|
|
||||||
|
def query_owner(id_value, id_type_column):
|
||||||
|
if not id_value:
|
||||||
|
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')
|
||||||
|
|
||||||
|
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}.")
|
||||||
|
return owner_by_anydesk
|
||||||
|
|
||||||
|
if owner_by_anydesk:
|
||||||
|
log.info(f"Владелец {owner_by_anydesk} найден по Anydesk ID: {anydesk_id}.")
|
||||||
|
return owner_by_anydesk
|
||||||
|
|
||||||
|
if owner_by_teamviewer:
|
||||||
|
log.info(f"Владелец {owner_by_teamviewer} найден по TeamViewer ID: {teamviewer_id}.")
|
||||||
|
return owner_by_teamviewer
|
||||||
|
|
||||||
|
log.warning(f"Владелец не найден для Anydesk={anydesk_id}, TV={teamviewer_id}. "
|
||||||
|
f"Будет назначен владелец по умолчанию: {config.DEFAULT_OWNER_UUID}")
|
||||||
|
return config.DEFAULT_OWNER_UUID
|
||||||
Reference in New Issue
Block a user