This commit is contained in:
2025-07-21 23:52:53 +03:00
parent 0413460c3a
commit f9e5d73868
22 changed files with 140 additions and 424 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ docker-compose.yml
*.db
__*
*.json
files/*

View File

@@ -115,7 +115,8 @@ class DatabaseManager:
uuid TEXT PRIMARY KEY,
owner_uuid TEXT,
clean_anydesk_id TEXT,
clean_teamviewer_id TEXT
clean_teamviewer_id TEXT,
lastModifiedDate TEXT
)""")
# Новая таблица для кэширования UUID справочников

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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 "
}

View File

@@ -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 "
}

View File

@@ -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 "
}

View File

@@ -79,7 +79,7 @@ class ServiceDeskClient:
"""Получает список всех рабочих станций."""
log.info("Запрос списка всех рабочих станций из ServiceDesk...")
params = {
'attrs': 'UUID,owner,AnyDesk,Teamviewer'
'attrs': 'UUID,owner,AnyDesk,Teamviewerm,lastModifiedDate'
}
workstations = self._make_request('POST', config.FIND_WORKSTATIONS_URL, params=params)
log.info(f"Получено {len(workstations)} записей о рабочих станциях.")

View File

@@ -17,9 +17,7 @@ log = logging.getLogger(__name__)
def _clean_sd_remote_id(raw_id: Optional[str]) -> Optional[str]:
"""
Очищает ID удаленного доступа, полученный из ServiceDesk.
(Эта функция была ранее в ftp_parser, но перенесена сюда,
так как "грязные" ID приходят из SD).
Очищает ID удаленного доступа, полученный из ServiceDesk..
"""
if not raw_id or raw_id.lower() == 'none':
return None
@@ -157,9 +155,10 @@ class Synchronizer:
ws['UUID'],
owner['UUID'],
_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. Логика синхронизации
self._update_existing_frs()
@@ -172,52 +171,110 @@ class Synchronizer:
def _update_existing_frs(self):
"""Находит и обновляет ФР с отличающимися датами."""
log.info("Поиск ФР для обновления даты окончания ФН...")
"""
Находит и обновляет ФР с отличающимися датами окончания ФН,
воспроизводя проверенную логику в более производительном виде.
"""
log.info("Поиск ФР для обновления...")
# 1. SQL-запрос для поиска расхождений.
# Он объединяет таблицы по serialNumber и выбирает все необходимые поля
# сразу, чтобы избежать вложенных циклов.
query = """
SELECT
pos.serialNumber,
pos.dateTime_end AS pos_date,
pos.datetime_reg AS pos_reg_date,
pos.dateTime_end AS pos_date_end,
pos.datetime_reg AS pos_date_reg,
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
sd.dateTime_end AS sd_date_end
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)} ФР для обновления.")
records_to_check = self.db._execute_query(query, fetch='all')
update_counter = 0
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']
}
for rec in records_to_check:
try:
self.sd.update_fr(rec['sd_uuid'], params_to_update)
except ServiceDeskAPIError as e:
log.error(f"Не удалось обновить ФР с UUID {rec['sd_uuid']}: {e}")
# 2. Приводим даты к одному "знаменателю" - объектам datetime
# dateutil.parser отлично справляется с разными форматами
# Проверяем, что обе даты существуют, прежде чем их парсить
if not rec['pos_date_end'] or not rec['sd_date_end']:
log.warning(f"Пропуск сравнения для S/N {rec['serialNumber']}: одна из дат отсутствует.")
continue
pos_date = parser.parse(rec['pos_date_end'])
sd_date = parser.parse(rec['sd_date_end'])
# 3. Сравниваем даты. Если они различаются, готовим обновление.
# Проверяем на неравенство. Величина различия не важна.
if pos_date != sd_date:
log.info(f"Найдено расхождение в дате для S/N {rec['serialNumber']} (UUID: {rec['sd_uuid']}). "
f"FTP: {pos_date}, SD: {sd_date}. Подготовка к обновлению.")
# 4. Формируем данные для обновления
# 5. Проверяем, закончился ли ФН
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 = {
'FNNumber': rec['pos_fn_serial'],
'FNExpireDate': formatted_expire_date,
'LegalName': legal_name,
'RNKKT': rec['pos_rnm'],
'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:
self.sd.update_fr(rec['sd_uuid'], params_to_update)
update_counter += 1
except ServiceDeskAPIError as 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):
"""Находит, определяет владельца и создает новые ФР."""
@@ -238,10 +295,14 @@ class Synchronizer:
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
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_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]):
log.error(f"Не удалось создать ФР с S/N {fr['serialNumber']}: не найдены UUID для справочников. "
@@ -270,20 +331,45 @@ class Synchronizer:
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 по умолчанию."""
"""
Ищет владельца по 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:
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:
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
if owner_by_anydesk:
@@ -297,3 +383,4 @@ class Synchronizer:
log.warning(f"Владелец не найден для Anydesk={anydesk_id}, TV={teamviewer_id}. "
f"Будет назначен владелец по умолчанию: {config.DEFAULT_OWNER_UUID}")
return config.DEFAULT_OWNER_UUID