release 1.1
This commit is contained in:
@@ -174,3 +174,4 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
.DS_Store
|
||||
@@ -69,3 +69,21 @@ async def getAllEmployees(host: str, port: int, identity: str, secret: str):
|
||||
offset += limit
|
||||
|
||||
return employees
|
||||
|
||||
|
||||
async def findPatientByPhone(
|
||||
host: str, port: int, identity: str, secret: str, phone: str
|
||||
):
|
||||
bearerToken = generate_token(identity, secret)
|
||||
headers = {"Authorization": f"Bearer {bearerToken}"}
|
||||
params = {
|
||||
"phone": phone,
|
||||
"limit": 10,
|
||||
"offset": 0,
|
||||
}
|
||||
return await requestGET(
|
||||
f"http://{host}:{port}/api/v2/clients",
|
||||
headers=headers,
|
||||
params=params,
|
||||
verify_ssl=False,
|
||||
)
|
||||
|
||||
+72
-56
@@ -1,4 +1,7 @@
|
||||
import config
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
|
||||
from db.schemas import Settings
|
||||
from utils import (
|
||||
logger,
|
||||
@@ -15,9 +18,48 @@ def getVerifySSL(url: str):
|
||||
return "demo" not in url.lower()
|
||||
|
||||
|
||||
# TODO Проработать лимит в 50 запросов за 15 минут
|
||||
def rate_limit(max_calls: int, period: int):
|
||||
"""
|
||||
Декоратор для ограничения количества вызовов функции
|
||||
:param max_calls: максимальное количество вызовов
|
||||
:param period: период в секундах
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
calls = []
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
now = datetime.now()
|
||||
# Удаляем старые вызовы
|
||||
calls[:] = [
|
||||
call_time
|
||||
for call_time in calls
|
||||
if now - call_time < timedelta(seconds=period)
|
||||
]
|
||||
|
||||
if len(calls) >= max_calls:
|
||||
oldest_call = calls[0]
|
||||
wait_time = (
|
||||
oldest_call + timedelta(seconds=period) - now
|
||||
).total_seconds()
|
||||
if wait_time > 0:
|
||||
logger.warning(
|
||||
f"Превышен лимит запросов. Ожидание {wait_time:.2f} секунд"
|
||||
)
|
||||
await asyncio.sleep(wait_time)
|
||||
# После ожидания рекурсивно вызываем функцию
|
||||
return await wrapper(*args, **kwargs)
|
||||
|
||||
calls.append(now)
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@rate_limit(max_calls=50, period=900)
|
||||
async def sendForSigning(inData: dict):
|
||||
settings = await Settings.getSettings(False)
|
||||
if not settings.success:
|
||||
@@ -29,9 +71,9 @@ async def sendForSigning(inData: dict):
|
||||
"idLpu": settings.n3healthXIdLpu,
|
||||
},
|
||||
"practitioner": inData.get("practitioner", {}),
|
||||
"patients": inData.get("patients", {}),
|
||||
"patients": [inData.get("patient", {}), *inData.get("recipients", [])],
|
||||
"medDocument": inData.get("medDocument", {}),
|
||||
"delivery": ["sms"],
|
||||
"delivery": [inData.get("deliveryType", "sms")],
|
||||
}
|
||||
|
||||
headers = {
|
||||
@@ -49,7 +91,7 @@ async def sendForSigning(inData: dict):
|
||||
return response
|
||||
|
||||
|
||||
async def resend(trackingID: str, IdPatientMis: str):
|
||||
async def resend(trackingID: str, IdsPatientMis: list[str], deliveryType: str = "sms"):
|
||||
settings = await Settings.getSettings(False)
|
||||
if not settings.success:
|
||||
logger.error("Настройки не инициализированы")
|
||||
@@ -61,8 +103,8 @@ async def resend(trackingID: str, IdPatientMis: str):
|
||||
}
|
||||
data = {
|
||||
"trackingId": trackingID,
|
||||
"IdPatientMis": [str(IdPatientMis)],
|
||||
"delivery": ["sms"],
|
||||
"IdPatientMis": [str(IdPatientMis) for IdPatientMis in IdsPatientMis],
|
||||
"delivery": [deliveryType],
|
||||
}
|
||||
response = await requestPOST(
|
||||
f"{settings.n3healthHost}/resend",
|
||||
@@ -108,32 +150,7 @@ async def checkConnection(host: str, authorization: str, xIdLpu: str):
|
||||
)
|
||||
|
||||
|
||||
async def getAllSigning():
|
||||
settings = await Settings.getSettings(False)
|
||||
if not settings.success:
|
||||
logger.error("Настройки не инициализированы")
|
||||
return answer(success=False, message="Настройки не инициализированы")
|
||||
apiUrl = settings.data.n3healthHost
|
||||
path = "/signing"
|
||||
headers = {"Authorization": config.N3_KEY, "X-Id-Lpu": config.N3_ID}
|
||||
response = await requestGET(apiUrl + path, headers=headers, verify_ssl=False)
|
||||
logger.info(response)
|
||||
return response
|
||||
|
||||
|
||||
async def getMisIdSigning(idPatientMis: str):
|
||||
apiUrl = config.N3_URL
|
||||
path = "/signing"
|
||||
headers = {"Authorization": config.N3_KEY, "X-Id-Lpu": config.N3_ID}
|
||||
parameters = {"idPatientMis": idPatientMis}
|
||||
response = await requestGET(
|
||||
apiUrl + path, headers=headers, params=parameters, verify_ssl=False
|
||||
)
|
||||
logger.info(response)
|
||||
return response
|
||||
|
||||
|
||||
async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId: int):
|
||||
async def getTrackingIDSigning(trackingID: str, singingId: int):
|
||||
settings = await Settings.getSettings(False)
|
||||
if not settings.success:
|
||||
logger.error("Настройки не инициализированы")
|
||||
@@ -154,24 +171,25 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId
|
||||
|
||||
patients = response.data.get("data", {}).get("patients", [])
|
||||
|
||||
status = {"code": 0, "description": ""}
|
||||
|
||||
patientsIds = []
|
||||
statuses = {"global": int(response.data.get("data", {}).get("status", 0))}
|
||||
|
||||
for patient in patients:
|
||||
patientsIds.append(patient["idPatientMis"])
|
||||
if patient.get("status", {}).get("code", 0) > status["code"]:
|
||||
status = patient.get("status", {})
|
||||
idPatientMis = patient.get("idPatientMis")
|
||||
if idPatientMis not in statuses:
|
||||
statuses[idPatientMis] = 0
|
||||
if patient.get("status", {}).get("code", 0) > statuses[idPatientMis]:
|
||||
statuses[idPatientMis] = patient.get("status", {}).get("code", 0)
|
||||
|
||||
message = "Файлы успешно сохранены"
|
||||
messages = []
|
||||
|
||||
if latestStatusCode != status["code"]:
|
||||
if statuses["global"] in [210, 211]:
|
||||
|
||||
attachments = (
|
||||
response.data.get("data", {}).get("medDocument", {}).get("attachments", [])
|
||||
)
|
||||
|
||||
if len(attachments) > 0:
|
||||
statuses["storagePath"] = []
|
||||
|
||||
for idx, attachment in enumerate(attachments, start=1):
|
||||
content = attachment.get("content")
|
||||
@@ -179,20 +197,18 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId
|
||||
continue
|
||||
|
||||
ATTACHMENTS_DIR = Path("src/attachments/import")
|
||||
result = detect_and_save_attachment(
|
||||
b64_content=content,
|
||||
output_dir=ATTACHMENTS_DIR / str(singingId),
|
||||
filename_prefix=f"document_{idx}",
|
||||
save_attachments=True,
|
||||
)
|
||||
|
||||
for patientId in patientsIds:
|
||||
if not result:
|
||||
message = f"Файл 'document_{idx}' не сохранены"
|
||||
logger.error(message)
|
||||
messages.append(message)
|
||||
else:
|
||||
statuses["storagePath"].append(str(result["path"]))
|
||||
|
||||
result = detect_and_save_attachment(
|
||||
b64_content=content,
|
||||
output_dir=ATTACHMENTS_DIR / patientId / str(singingId),
|
||||
filename_prefix=f"document_{idx}",
|
||||
save_attachments=True,
|
||||
)
|
||||
|
||||
if not result:
|
||||
logger.error("Файлы не сохранены")
|
||||
message = "Файлы не сохранены"
|
||||
else:
|
||||
status["storagePath"] = str(result["path"])
|
||||
|
||||
return answer(data=status, message=message)
|
||||
return answer(data=statuses, message=", ".join(messages))
|
||||
|
||||
+231
-71
@@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from api.medods import findPatientByPhone
|
||||
from api.n3health import resend, revoke, sendForSigning
|
||||
from db.schemas.documents import Document
|
||||
from db.schemas.patients import Patient
|
||||
@@ -37,9 +38,13 @@ async def pre_singing(
|
||||
signature["expiration"] = practitioner.data.get("expired_at")[:10]
|
||||
|
||||
daysRemainingControl = 0
|
||||
deliveryType = None
|
||||
settings = await Settings.getSettings()
|
||||
if settings.success:
|
||||
daysRemainingControl = settings.data.get("expirationAlert", 0)
|
||||
deliveryData = settings.data.get("deliveryType", {})
|
||||
deliveryType = [k for k, v in deliveryData.items() if v]
|
||||
multipleSending = settings.data.get("multipleSending", False)
|
||||
|
||||
inProgress = []
|
||||
complete = []
|
||||
@@ -75,6 +80,8 @@ async def pre_singing(
|
||||
"inProgress": inProgress,
|
||||
"complete": complete,
|
||||
"daysRemainingControl": daysRemainingControl,
|
||||
"deliveryType": deliveryType,
|
||||
"multipleSending": multipleSending,
|
||||
}
|
||||
return exitData
|
||||
|
||||
@@ -85,8 +92,9 @@ async def singing(
|
||||
):
|
||||
def correctData(data):
|
||||
data["practitioner"]["userIdLpu"] = str(data["practitioner"]["userIdLpu"])
|
||||
for patient in data["patients"]:
|
||||
patient["idPatientMis"] = str(patient["idPatientMis"])
|
||||
data["patient"]["idPatientMis"] = str(data["patient"]["idPatientMis"])
|
||||
for recipient in data.get("recipients", []):
|
||||
recipient["idPatientMis"] = str(recipient["idPatientMis"])
|
||||
return data
|
||||
|
||||
logger.info(f"📥 Получен запрос на /singing")
|
||||
@@ -110,57 +118,109 @@ async def singing(
|
||||
return {"status": "ERROR", "message": "Сотрудник не найден"}
|
||||
esiaAuth = practitioner.data.get("esiaAuth", False)
|
||||
userIdLpu = practitioner.data.get("userIdLpu", 0)
|
||||
body["practitioner"]["snils"] = practitioner.data.get("snils")
|
||||
if not esiaAuth:
|
||||
body["practitioner"]["mrProxyNumber"] = practitioner.data.get("attorney")
|
||||
|
||||
patients = body.get("patients", [])
|
||||
if len(patients) == 0:
|
||||
patient = body.get("patient", None)
|
||||
if not patient:
|
||||
logger.error("Данные пациента не получены, пациент не найден")
|
||||
return {
|
||||
"status": "ERROR",
|
||||
"message": "Данные пациента не получены, пациент не найден",
|
||||
}
|
||||
idPatientMis = patient.get("idPatientMis", None)
|
||||
|
||||
patientsData = []
|
||||
if not idPatientMis:
|
||||
logger.error("Не указан идентификатор пациента")
|
||||
return {"status": "ERROR", "message": "Не указан идентификатор пациента"}
|
||||
patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True)
|
||||
if not patientDB.success:
|
||||
patientData = {
|
||||
"idPatientMis": idPatientMis,
|
||||
"familyName": patient.get("familyName", "N/A"),
|
||||
"givenName": patient.get("givenName", "N/A"),
|
||||
"middleName": patient.get("middleName", "N/A"),
|
||||
"birthDate": patient.get("birthDate", "N/A"),
|
||||
"sex": patient.get("sex", "N/A"),
|
||||
}
|
||||
newPatient = await Patient.addPatient(**patientData)
|
||||
if not newPatient.success:
|
||||
return {
|
||||
"status": "ERROR",
|
||||
"message": "Не удалось создать пациента",
|
||||
}
|
||||
patientData = newPatient.data.toDict()
|
||||
else:
|
||||
patientData = patientDB.data
|
||||
|
||||
for patient in patients:
|
||||
idPatientMis = patient.get("idPatientMis", None)
|
||||
if not idPatientMis:
|
||||
recipients = body.pop("recipients", [])
|
||||
|
||||
recipientsData = []
|
||||
recipientsIds = []
|
||||
|
||||
for recipient in recipients:
|
||||
idRecipientMis = recipient.get("idPatientMis", None)
|
||||
if not idRecipientMis:
|
||||
logger.error("Не указан идентификатор пациента")
|
||||
return {
|
||||
"status": "ERROR",
|
||||
"message": "Не указан идентификатор пациента",
|
||||
}
|
||||
patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True)
|
||||
if not patientDB.success:
|
||||
patientData = {
|
||||
"idPatientMis": idPatientMis,
|
||||
"familyName": patient.get("familyName", "N/A"),
|
||||
"givenName": patient.get("givenName", "N/A"),
|
||||
"middleName": patient.get("middleName", "N/A"),
|
||||
"birthDate": patient.get("birthDate", "N/A"),
|
||||
"sex": patient.get("sex", "N/A"),
|
||||
recipientDB = await Patient.getPatientByIdPatientMis(
|
||||
idRecipientMis, isCheck=True
|
||||
)
|
||||
if not recipientDB.success:
|
||||
recipientData = {
|
||||
"idPatientMis": idRecipientMis,
|
||||
"familyName": recipient.get("surname", "N/A"),
|
||||
"givenName": recipient.get("name", "N/A"),
|
||||
"middleName": recipient.get("secondName", "N/A"),
|
||||
"birthDate": recipient.get("birthDate", "N/A"),
|
||||
"sex": recipient.get("sex", "N/A"),
|
||||
}
|
||||
newPatient = await Patient.addPatient(**patientData)
|
||||
if not newPatient.success:
|
||||
newRecipient = await Patient.addPatient(**recipientData)
|
||||
if not newRecipient.success:
|
||||
return {
|
||||
"status": "ERROR",
|
||||
"message": newPatient.message,
|
||||
"message": newRecipient.message,
|
||||
}
|
||||
patientsData.append(newPatient.data.toDict())
|
||||
recipientsData.append(newRecipient.data.toDict())
|
||||
recipientsIds.append(newRecipient.data.idPatientMis)
|
||||
else:
|
||||
patientsData.append(patientDB.data)
|
||||
logger.info(recipientDB.data)
|
||||
recipientsData.append(recipientDB.data)
|
||||
recipientsIds.append(recipientDB.data.get("idPatientMis"))
|
||||
|
||||
recipient["documentDto"] = [
|
||||
{
|
||||
"docN": recipient.pop("snils"),
|
||||
"docS": "",
|
||||
"documentName": "СНИЛС",
|
||||
"idDocumentType": 223,
|
||||
"providerName": "ПФР",
|
||||
}
|
||||
]
|
||||
recipient["telecom"] = [
|
||||
{
|
||||
"system": "Telephone",
|
||||
"value": f'+{recipient.pop("phone")}',
|
||||
}
|
||||
]
|
||||
body["recipients"] = recipients
|
||||
|
||||
# Логируем информацию о документах
|
||||
logger.info(f"👨⚕️ Медработник: {practitioner.data.get("name")}")
|
||||
logger.info(f"👤 Пациент: {patientData.get('name')}")
|
||||
logger.info(
|
||||
f"👥 Пациенты ({len(patientsData)}): {', '.join([f'{p.get('name')}' for p in patientsData])}"
|
||||
f"👥 Получатели ({len(recipientsData)}): {', '.join([f'{p.get('name')}' for p in recipientsData])}"
|
||||
)
|
||||
logger.info(f"📄 Документов для обработки: {len(docs)}")
|
||||
logger.info(f"🔐 ЕСИА: {esiaAuth}")
|
||||
|
||||
try:
|
||||
# Конвертируем документы в PDF
|
||||
conversion_result = convert_docs_to_pdfs(docs, patientsData)
|
||||
# logger.info(conversion_result)
|
||||
conversion_result = convert_docs_to_pdfs(docs, idPatientMis)
|
||||
|
||||
if conversion_result.get("status", "") != "SUCCESS":
|
||||
return {
|
||||
@@ -168,7 +228,9 @@ async def singing(
|
||||
"message": "Не удалось сохранить документы",
|
||||
}
|
||||
|
||||
newSinging = await Signing.addSigning(userIdLpu, idPatientMis)
|
||||
newSinging = await Signing.addSigning(
|
||||
userIdLpu, idPatientMis, body["deliveryType"], recipientsIds
|
||||
)
|
||||
if not newSinging:
|
||||
return {
|
||||
"status": "ERROR",
|
||||
@@ -176,37 +238,41 @@ async def singing(
|
||||
}
|
||||
logger.info(f"🔐 Подписание добавлено: {newSinging.id}")
|
||||
|
||||
pdfFiles = conversion_result.get("files", {})
|
||||
pdfFiles = conversion_result.get("files", [])
|
||||
body["medDocument"] = {
|
||||
"esiaAuth": esiaAuth,
|
||||
"attachments": [],
|
||||
}
|
||||
body["files"] = []
|
||||
for key in pdfFiles:
|
||||
pdfFile = pdfFiles[key]
|
||||
for doc in pdfFile:
|
||||
for doc in pdfFiles:
|
||||
for idPatient in [idPatientMis, *recipientsIds]:
|
||||
newDocument = await Document.addDocument(
|
||||
singingId=newSinging.id, idPatientMis=key, **doc
|
||||
singingId=newSinging.id, idPatientMis=str(idPatient), **doc
|
||||
)
|
||||
if not newDocument:
|
||||
logger.error(
|
||||
f"Не удалось добавить документ для получателя {idPatient}"
|
||||
)
|
||||
return {
|
||||
"status": "ERROR",
|
||||
"message": "Не удалось добавить документ",
|
||||
}
|
||||
logger.info(f"📄 Документ добавлен: {newDocument.id}")
|
||||
partName = f'file{len(body["medDocument"]["attachments"])}'
|
||||
body["medDocument"]["attachments"].append(
|
||||
{
|
||||
"partName": partName,
|
||||
}
|
||||
)
|
||||
body["files"].append(
|
||||
{
|
||||
"field": partName,
|
||||
"path": Path(doc["storagePath"]),
|
||||
"content_type": "application/pdf",
|
||||
}
|
||||
logger.debug(
|
||||
f"📄 Документ добавлен: {newDocument.id} для получателя {idPatient}"
|
||||
)
|
||||
partName = f'file{len(body["medDocument"]["attachments"])}'
|
||||
body["medDocument"]["attachments"].append(
|
||||
{
|
||||
"partName": partName,
|
||||
}
|
||||
)
|
||||
body["files"].append(
|
||||
{
|
||||
"field": partName,
|
||||
"path": Path(doc["storagePath"]),
|
||||
"content_type": "application/pdf",
|
||||
}
|
||||
)
|
||||
|
||||
if esiaAuth:
|
||||
partName = f'file{len(body["medDocument"]["attachments"])}'
|
||||
@@ -234,7 +300,7 @@ async def singing(
|
||||
|
||||
newSinging.trackingId = signingResult.get("trackingId", "")
|
||||
await newSinging.save()
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f"🔐 Подписание обновлено: {newSinging.id}. Добавлен Трек-номер подписания"
|
||||
)
|
||||
|
||||
@@ -242,10 +308,9 @@ async def singing(
|
||||
|
||||
logger.info(f"✅ Обработка завершена!")
|
||||
|
||||
# Возвращаем ответ
|
||||
response = {
|
||||
"status": "SUCCESS",
|
||||
"message": f"Документы успешно обработаны. Трек-номер подписания: {newSinging.trackingId}",
|
||||
"message": "Документы успешно обработаны.",
|
||||
}
|
||||
|
||||
return response
|
||||
@@ -274,6 +339,16 @@ async def statuses(
|
||||
result = await Signing.getFilteredSingings(userIdLpu, filters)
|
||||
singingsData = result.data
|
||||
|
||||
for singingData in singingsData:
|
||||
documents = singingData.pop("documents", [])
|
||||
uniqueDocs = []
|
||||
uniquePaths = []
|
||||
for doc in documents:
|
||||
if doc.get("storagePath") not in uniquePaths:
|
||||
uniquePaths.append(doc.get("storagePath"))
|
||||
uniqueDocs.append(doc)
|
||||
singingData["documents"] = uniqueDocs
|
||||
|
||||
return singingsData
|
||||
|
||||
|
||||
@@ -290,7 +365,7 @@ async def documents(
|
||||
logger.error("Не отправлен idPatientMis")
|
||||
return {"status": "ERROR", "message": "Не отправлен idPatientMis"}
|
||||
|
||||
idPatientMis = body.get("idPatientMis")
|
||||
idPatientMis = str(body.get("idPatientMis"))
|
||||
|
||||
singingData = await Signing.getSigningsByIdPatientMis(idPatientMis)
|
||||
|
||||
@@ -308,7 +383,38 @@ async def documents(
|
||||
for s in singingData.data:
|
||||
s["practitioner"] = practitioners[s.get("userIdLpu")]
|
||||
|
||||
responseData = {"status": "SUCCESS", "data": singingData.data}
|
||||
for singing in singingData.data:
|
||||
statusesData = [
|
||||
{d.get("idPatientMis"): d.get("patient", {}).get("name")}
|
||||
for d in singing.get("statuses", [])
|
||||
if d.get("idPatientMis")
|
||||
]
|
||||
idMatchName = {}
|
||||
for statusData in statusesData:
|
||||
for idPatientMisStatus, name in statusData.items():
|
||||
if idPatientMisStatus in idMatchName:
|
||||
continue
|
||||
idMatchName[idPatientMisStatus] = name
|
||||
|
||||
documents = singing.pop("documents")
|
||||
uniqueDocs = []
|
||||
for doc in documents:
|
||||
docOwner = doc.get("storagePath").split("/")[-2:-1][0]
|
||||
isOwn = docOwner == idPatientMis
|
||||
if str(doc.get("idPatientMis")) == idPatientMis:
|
||||
if not isOwn:
|
||||
ownerName = idMatchName[docOwner]
|
||||
doc["title"] = (
|
||||
f"{doc.get('title')} (Пациент: {idMatchName[docOwner]})"
|
||||
)
|
||||
uniqueDocs.append(doc)
|
||||
singing["documents"] = uniqueDocs
|
||||
|
||||
settings = await Settings.getSettings(False)
|
||||
|
||||
data = {"deliveryType": settings.data.deliveryType, "singings": singingData.data}
|
||||
|
||||
responseData = {"status": "SUCCESS", "data": data}
|
||||
|
||||
return responseData
|
||||
|
||||
@@ -335,6 +441,7 @@ async def revoke_post(
|
||||
reqData: dict = Depends(requestDict),
|
||||
):
|
||||
logger.info(f"📥 Получен запрос на /revoke")
|
||||
|
||||
trackingId = reqData.get("body", {}).get("trackingId", None)
|
||||
if not trackingId:
|
||||
return {"status": "ERROR", "message": "Документ не найден"}
|
||||
@@ -359,35 +466,25 @@ async def resend_post(
|
||||
reqData: dict = Depends(requestDict),
|
||||
):
|
||||
logger.info(f"📥 Получен запрос на /resend")
|
||||
|
||||
trackingId = reqData.get("body", {}).get("trackingId", None)
|
||||
idPatientMis = reqData.get("body", {}).get("idPatientMis", None)
|
||||
deliveryType = reqData.get("body", {}).get("deliveryType", None)
|
||||
|
||||
errorResponse = {"status": "ERROR", "message": "Повторная отправка не удалось"}
|
||||
if not trackingId or not idPatientMis:
|
||||
logger.error("Не отправлен trackingId или idPatientMis")
|
||||
logger.error(f"trackingId: {trackingId}, idPatientMis: {idPatientMis}")
|
||||
if not trackingId or not idPatientMis or not deliveryType:
|
||||
logger.error("Не отправлен trackingId или idPatientMis или deliveryType")
|
||||
logger.error(
|
||||
f"trackingId: {trackingId}, idPatientMis: {idPatientMis}, deliveryType: {deliveryType}"
|
||||
)
|
||||
return errorResponse
|
||||
|
||||
resendResult = await resend(trackingId, idPatientMis)
|
||||
resendResult = await resend(trackingId, idPatientMis, deliveryType)
|
||||
|
||||
if not resendResult or not resendResult.get("status", ""):
|
||||
if not resendResult or not resendResult.get("success", False):
|
||||
logger.error(resendResult)
|
||||
return errorResponse
|
||||
|
||||
newTrackingId = resendResult.get("tracking_id", "")
|
||||
singingId = reqData.get("body", {}).get("id", "")
|
||||
|
||||
if not newTrackingId or not singingId:
|
||||
logger.error("Не отправлен trackingId или id")
|
||||
logger.error(f"trackingId: {newTrackingId}, id: {singingId}")
|
||||
return errorResponse
|
||||
|
||||
singing = await Signing.getSigningById(singingId)
|
||||
if not singing.success:
|
||||
logger.error(singing.message)
|
||||
return errorResponse
|
||||
|
||||
await singing.data.edit(trackingId=newTrackingId)
|
||||
logger.info("✅ Повторная отправка успешна")
|
||||
|
||||
return {"status": "SUCCESS", "message": resendResult.get("message", "")}
|
||||
@@ -399,7 +496,70 @@ async def advanced_search_post(
|
||||
):
|
||||
logger.info(f"📥 Получен запрос на /advanced-search")
|
||||
|
||||
result = await Signing.getFilteredSingings(
|
||||
"", reqData.get("body", {}).get("filters", {})
|
||||
)
|
||||
filters = reqData.get("body", {}).get("filters", {})
|
||||
result = await Signing.getFilteredSingings("", filters)
|
||||
|
||||
for singingData in result.data:
|
||||
documents = singingData.pop("documents", [])
|
||||
uniqueDocs = []
|
||||
uniquePaths = []
|
||||
for doc in documents:
|
||||
if doc.get("storagePath") not in uniquePaths:
|
||||
uniquePaths.append(doc.get("storagePath"))
|
||||
uniqueDocs.append(doc)
|
||||
singingData["documents"] = uniqueDocs
|
||||
|
||||
return {"status": "SUCCESS", "data": result.data}
|
||||
|
||||
|
||||
@router.post("/search-recipients", summary="search-recipients")
|
||||
async def search_recipients_post(
|
||||
reqData: dict = Depends(requestDict),
|
||||
):
|
||||
logger.info(f"📥 Получен запрос на /search-recipients")
|
||||
phoneNumber = "".join(
|
||||
filter(str.isdigit, reqData.get("body", {}).get("phone", ""))
|
||||
)[-10:]
|
||||
idPatientMis = str(reqData.get("body", {}).get("idPatientMis", ""))
|
||||
|
||||
settings = await Settings.getSettings(False)
|
||||
if not settings.success:
|
||||
logger.error(settings.error)
|
||||
return {"status": "error", "message": settings.error}
|
||||
settings = settings.data
|
||||
try:
|
||||
patientsDB = await findPatientByPhone(
|
||||
settings.medodsApiHost,
|
||||
settings.medodsApiPort,
|
||||
settings.medodsApiIdentity,
|
||||
settings.medodsApiSecretKey,
|
||||
f"7{phoneNumber}",
|
||||
)
|
||||
if not patientsDB.success or patientsDB.data.get("totalItems") == 0:
|
||||
logger.error("Пациент не найден")
|
||||
return {"status": "error", "data": []}
|
||||
patients = patientsDB.data.get("data", [])
|
||||
result = [
|
||||
{
|
||||
"idPatientMis": str(patient.get("id")),
|
||||
"name": patient.get("name"),
|
||||
"surname": patient.get("surname"),
|
||||
"secondName": patient.get("secondName"),
|
||||
"birthDate": patient.get("birthdate"),
|
||||
"sex": patient.get("sex"),
|
||||
"snils": patient.get("snils"),
|
||||
"phone": patient.get("phone"),
|
||||
}
|
||||
for patient in patients
|
||||
if str(patient.get("id")) != idPatientMis
|
||||
]
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": result,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return {
|
||||
"status": "error",
|
||||
"data": [],
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ async def settingsPage(request: Request):
|
||||
@router.get("/get", name="getSettings", summary="Получить настройки")
|
||||
async def getSettings():
|
||||
settings = await Settings.getSettings()
|
||||
# logger.info(settings.data)
|
||||
return settings.data
|
||||
|
||||
|
||||
@@ -33,6 +34,30 @@ async def updatePassword(reqData: dict = Depends(requestDict)):
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/update-sending", name="updateSending", summary="Обновить настройку sending"
|
||||
)
|
||||
async def updateSending(reqData: dict = Depends(requestDict)):
|
||||
logger.info(f"📥 Получен запрос на /update-sending")
|
||||
response = {"status": "error"}
|
||||
sendingData = reqData.get("body", {})
|
||||
await Settings.updateSettings(**sendingData)
|
||||
response["status"] = "ok"
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/update-delivery", name="updateDelivery", summary="Обновить настройку delivery"
|
||||
)
|
||||
async def updateDelivery(reqData: dict = Depends(requestDict)):
|
||||
logger.info(f"📥 Получен запрос на /update-delivery")
|
||||
response = {"status": "error"}
|
||||
deliveryType = reqData.get("body", {})
|
||||
await Settings.updateSettings(deliveryType=deliveryType)
|
||||
response["status"] = "ok"
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/test-medods", name="testMedods", summary="Проверить соединение с Медодс")
|
||||
async def testMedods(reqData: dict = Depends(requestDict)):
|
||||
logger.info(f"📥 Получен запрос на /test-medods")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
let systemStatus = {
|
||||
medods: { configured: false, details: {} },
|
||||
n3health: { configured: false, details: {} },
|
||||
password: { configured: true }
|
||||
delivery: { configured: false, types: {} } // Добавлен новый объект для способов доставки
|
||||
};
|
||||
|
||||
// Глобальные переменные для статистики сотрудников
|
||||
@@ -44,6 +44,16 @@ async function loadSystemStatus() {
|
||||
hasLpuId: !!data.n3healthXIdLpu
|
||||
};
|
||||
|
||||
// Обновляем статус способов доставки
|
||||
if (data.deliveryType) {
|
||||
systemStatus.delivery.types = data.deliveryType;
|
||||
// Проверяем, что хотя бы один способ активен
|
||||
systemStatus.delivery.configured = Object.values(data.deliveryType).some(v => v === true);
|
||||
} else {
|
||||
systemStatus.delivery.types = { max: false, sms: false, mila: false, goskey: false };
|
||||
systemStatus.delivery.configured = false;
|
||||
}
|
||||
|
||||
// Обновляем UI
|
||||
updateStatusUI();
|
||||
updateReadinessIndicator();
|
||||
@@ -227,6 +237,50 @@ function updateStatusUI() {
|
||||
<div class="text-danger"><i class="bi bi-exclamation-circle"></i> Идентификатор МО: нет</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Способы доставки - с обновленными иконками
|
||||
const deliveryBadge = document.getElementById('deliveryStatusBadge');
|
||||
const deliveryDetails = document.getElementById('deliveryDetails');
|
||||
const types = systemStatus.delivery.types;
|
||||
const activeTypes = [];
|
||||
|
||||
if (types.max) activeTypes.push('MAX');
|
||||
if (types.sms) activeTypes.push('SMS');
|
||||
if (types.mila) activeTypes.push('MILA');
|
||||
if (types.goskey) activeTypes.push('GOSKEY');
|
||||
|
||||
if (systemStatus.delivery.configured && activeTypes.length > 0) {
|
||||
deliveryBadge.className = 'badge bg-success';
|
||||
deliveryBadge.textContent = `Активны (${activeTypes.length})`;
|
||||
|
||||
let detailsHtml = '';
|
||||
const allTypes = [
|
||||
{ name: 'MAX', active: types.max, icon: 'bi-chat-fill', desc: 'Активно' },
|
||||
{ name: 'SMS', active: types.sms, icon: 'bi-envelope-fill', desc: 'Активно' },
|
||||
{ name: 'MILA', active: types.mila, icon: 'bi-shield-fill', desc: 'Активно' },
|
||||
{ name: 'GOSKEY', active: types.goskey, icon: 'bi-key-fill', desc: 'Активно' }
|
||||
];
|
||||
|
||||
allTypes.forEach(type => {
|
||||
if (type.active) {
|
||||
detailsHtml += `<div class="text-success mb-1"><i class="bi ${type.icon} me-1"></i> ${type.name} <span class="text-muted">(${type.desc})</span></div>`;
|
||||
} else {
|
||||
detailsHtml += `<div class="text-secondary mb-1"><i class="bi ${type.icon} me-1"></i> ${type.name} <span class="text-muted">(неактивен)</span></div>`;
|
||||
}
|
||||
});
|
||||
|
||||
deliveryDetails.innerHTML = detailsHtml;
|
||||
} else {
|
||||
deliveryBadge.className = 'badge bg-danger';
|
||||
deliveryBadge.textContent = 'Не настроены';
|
||||
|
||||
deliveryDetails.innerHTML = `
|
||||
<div class="text-danger mb-1"><i class="bi bi-chat-fill me-1"></i> MAX (чат) - неактивен</div>
|
||||
<div class="text-danger mb-1"><i class="bi bi-envelope-fill me-1"></i> SMS - неактивен</div>
|
||||
<div class="text-danger mb-1"><i class="bi bi-shield-fill me-1"></i> MILA - неактивен</div>
|
||||
<div class="text-danger"><i class="bi bi-key-fill me-1"></i> GOSKEY - неактивен</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление индикатора готовности и управление кнопкой
|
||||
@@ -236,9 +290,9 @@ function updateReadinessIndicator() {
|
||||
const settingsButtonContainer = document.getElementById('settingsButtonContainer');
|
||||
|
||||
let configuredCount = 0;
|
||||
if (systemStatus.password.configured) configuredCount++;
|
||||
if (systemStatus.medods.configured) configuredCount++;
|
||||
if (systemStatus.n3health.configured) configuredCount++;
|
||||
if (systemStatus.delivery.configured) configuredCount++; // Заменили password на delivery
|
||||
|
||||
const readinessPercentage = Math.round((configuredCount / 3) * 100);
|
||||
|
||||
|
||||
@@ -9,12 +9,37 @@ let currentSettings = {
|
||||
host: '',
|
||||
authorization: '',
|
||||
xIdLpu: ''
|
||||
}
|
||||
},
|
||||
delivery: { // Добавьте эту секцию
|
||||
max: false,
|
||||
sms: false,
|
||||
mila: false,
|
||||
goskey: false
|
||||
},
|
||||
multipleSending: false
|
||||
};
|
||||
|
||||
// Состояния изменений
|
||||
let medodsChanged = false;
|
||||
let n3healthChanged = false;
|
||||
let deliveryChanged = false;
|
||||
let sendingChanged = false;
|
||||
|
||||
// функцию обновления кнопок отправки
|
||||
function updateSendingButtons() {
|
||||
const saveBtn = document.getElementById('saveSendingBtn');
|
||||
const cancelBtn = document.getElementById('cancelSendingBtn');
|
||||
|
||||
saveBtn.disabled = !sendingChanged;
|
||||
cancelBtn.disabled = !sendingChanged;
|
||||
}
|
||||
|
||||
// функцию сброса формы отправки
|
||||
function resetSendingForm() {
|
||||
document.getElementById('multipleSending').checked = currentSettings.multipleSending;
|
||||
sendingChanged = false;
|
||||
updateSendingButtons();
|
||||
}
|
||||
|
||||
// Утилиты
|
||||
function showAlert(message, type = 'success') {
|
||||
@@ -42,6 +67,46 @@ function showAlert(message, type = 'success') {
|
||||
}
|
||||
}
|
||||
|
||||
// Добавьте функцию проверки выбора способов доставки:
|
||||
function isDeliveryValid() {
|
||||
const checkboxes = document.querySelectorAll('.delivery-checkbox');
|
||||
let checkedCount = 0;
|
||||
checkboxes.forEach(cb => {
|
||||
if (cb.checked) checkedCount++;
|
||||
});
|
||||
|
||||
const validationMessage = document.getElementById('deliveryValidationMessage');
|
||||
if (checkedCount === 0) {
|
||||
validationMessage.style.display = 'block';
|
||||
return false;
|
||||
} else {
|
||||
validationMessage.style.display = 'none';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Добавьте функцию обновления кнопок доставки:
|
||||
function updateDeliveryButtons() {
|
||||
const isValid = isDeliveryValid();
|
||||
const saveBtn = document.getElementById('saveDeliveryBtn');
|
||||
const cancelBtn = document.getElementById('cancelDeliveryBtn');
|
||||
|
||||
saveBtn.disabled = !isValid || !deliveryChanged;
|
||||
cancelBtn.disabled = !deliveryChanged;
|
||||
}
|
||||
|
||||
// Добавьте функцию сброса формы доставки:
|
||||
function resetDeliveryForm() {
|
||||
document.getElementById('deliveryMax').checked = currentSettings.delivery.max;
|
||||
document.getElementById('deliverySms').checked = currentSettings.delivery.sms;
|
||||
document.getElementById('deliveryMila').checked = currentSettings.delivery.mila;
|
||||
document.getElementById('deliveryGoskey').checked = currentSettings.delivery.goskey;
|
||||
|
||||
deliveryChanged = false;
|
||||
isDeliveryValid(); // Обновляем сообщение валидации
|
||||
updateDeliveryButtons();
|
||||
}
|
||||
|
||||
// Проверка заполненности формы Медодс
|
||||
function isMedodsFormValid() {
|
||||
const host = document.getElementById('medodsHost').value;
|
||||
@@ -94,6 +159,30 @@ function updateStatusIndicators() {
|
||||
n3healthStatus.className = 'badge bg-danger';
|
||||
n3healthStatus.textContent = 'Требуется настройка';
|
||||
}
|
||||
|
||||
// Способы доставки - обновляем статус
|
||||
const deliveryStatus = document.getElementById('deliveryStatus');
|
||||
const hasDeliveryConfig = Object.values(currentSettings.delivery).some(v => v === true);
|
||||
|
||||
if (hasDeliveryConfig) {
|
||||
deliveryStatus.className = 'badge bg-success';
|
||||
// Показываем количество активных способов
|
||||
const activeCount = Object.values(currentSettings.delivery).filter(v => v).length;
|
||||
deliveryStatus.textContent = `${activeCount} активн${activeCount === 1 ? 'ый' : 'ых'}`;
|
||||
} else {
|
||||
deliveryStatus.className = 'badge bg-danger';
|
||||
deliveryStatus.textContent = 'Не настроен';
|
||||
}
|
||||
|
||||
// Настройки отправки - НОВЫЙ БЛОК
|
||||
const sendingStatus = document.getElementById('sendingStatus');
|
||||
if (currentSettings.multipleSending) {
|
||||
sendingStatus.className = 'badge bg-success';
|
||||
sendingStatus.textContent = 'Включено';
|
||||
} else {
|
||||
sendingStatus.className = 'badge bg-secondary';
|
||||
sendingStatus.textContent = 'Отключено';
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление кнопок Медодс
|
||||
@@ -219,6 +308,21 @@ async function loadCurrentSettings() {
|
||||
document.getElementById('n3healthXIdLpu').value = data.n3healthXIdLpu;
|
||||
}
|
||||
|
||||
// Загружаем настройки доставки
|
||||
if (data.deliveryType) {
|
||||
currentSettings.delivery = { ...currentSettings.delivery, ...data.deliveryType };
|
||||
document.getElementById('deliveryMax').checked = data.deliveryType.max || false;
|
||||
document.getElementById('deliverySms').checked = data.deliveryType.sms || false;
|
||||
document.getElementById('deliveryMila').checked = data.deliveryType.mila || false;
|
||||
document.getElementById('deliveryGoskey').checked = data.deliveryType.goskey || false;
|
||||
}
|
||||
|
||||
// Загружаем настройку множественной отправки - НОВЫЙ БЛОК
|
||||
if (data.multipleSending !== undefined) {
|
||||
currentSettings.multipleSending = data.multipleSending;
|
||||
document.getElementById('multipleSending').checked = data.multipleSending;
|
||||
}
|
||||
|
||||
updateStatusIndicators();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -264,6 +368,22 @@ function setupChangeListeners() {
|
||||
updateN3HealthButtons();
|
||||
});
|
||||
});
|
||||
|
||||
// Способы доставки
|
||||
const deliveryCheckboxes = document.querySelectorAll('.delivery-checkbox');
|
||||
deliveryCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', () => {
|
||||
deliveryChanged = true;
|
||||
isDeliveryValid();
|
||||
updateDeliveryButtons();
|
||||
});
|
||||
});
|
||||
|
||||
// Настройки отправки - НОВЫЙ СЛУШАТЕЛЬ
|
||||
document.getElementById('multipleSending').addEventListener('change', () => {
|
||||
sendingChanged = true;
|
||||
updateSendingButtons();
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
@@ -532,4 +652,102 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// Обработчик отмены изменений N3Health
|
||||
document.getElementById('cancelN3HealthBtn').addEventListener('click', resetN3HealthForm);
|
||||
|
||||
// Обработчик сохранения настроек доставки
|
||||
document.getElementById('deliveryForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isDeliveryValid()) {
|
||||
showAlert('Должен быть выбран хотя бы один способ доставки', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const deliveryData = {
|
||||
max: document.getElementById('deliveryMax').checked,
|
||||
sms: document.getElementById('deliverySms').checked,
|
||||
mila: document.getElementById('deliveryMila').checked,
|
||||
goskey: document.getElementById('deliveryGoskey').checked
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/update-delivery', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(deliveryData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.status === "ok") {
|
||||
showAlert('Настройки способов доставки успешно сохранены', 'success');
|
||||
|
||||
// Обновляем текущие настройки
|
||||
currentSettings.delivery = { ...deliveryData };
|
||||
|
||||
// Сбрасываем флаги изменений
|
||||
deliveryChanged = false;
|
||||
|
||||
// Обновляем кнопки и статусы
|
||||
updateDeliveryButtons();
|
||||
updateStatusIndicators();
|
||||
} else {
|
||||
showAlert(data.error || 'Ошибка при сохранении', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
showAlert('Ошибка при сохранении', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик отмены изменений доставки
|
||||
document.getElementById('cancelDeliveryBtn').addEventListener('click', resetDeliveryForm);
|
||||
|
||||
// Обработчик сохранения настроек отправки - НОВЫЙ
|
||||
document.getElementById('sendingForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const multipleSending = document.getElementById('multipleSending').checked;
|
||||
|
||||
try {
|
||||
const response = await fetch('/settings/update-sending', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
multipleSending: multipleSending
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.status === "ok") {
|
||||
showAlert('Настройки отправки успешно сохранены', 'success');
|
||||
|
||||
// Обновляем текущие настройки
|
||||
currentSettings.multipleSending = multipleSending;
|
||||
|
||||
// Сбрасываем флаги изменений
|
||||
sendingChanged = false;
|
||||
|
||||
// Обновляем кнопки и статусы
|
||||
updateSendingButtons();
|
||||
updateStatusIndicators();
|
||||
} else {
|
||||
showAlert(data.error || 'Ошибка при сохранении', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка:', error);
|
||||
showAlert('Ошибка при сохранении', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик отмены изменений отправки - НОВЫЙ
|
||||
document.getElementById('cancelSendingBtn').addEventListener('click', resetSendingForm);
|
||||
|
||||
// Инициализация состояния кнопок доставки
|
||||
updateDeliveryButtons();
|
||||
updateSendingButtons();
|
||||
});
|
||||
@@ -368,11 +368,17 @@ function updateStaffTable() {
|
||||
} else {
|
||||
// Экранируем специальные символы в attorney
|
||||
const attorney = (practitioner.attorney || '').replace(/"/g, '"').replace(/'/g, ''');
|
||||
const eyeIcon = `<button type="button" class="btn btn-sm btn-link p-0 ms-1"
|
||||
const eyeIcon = `<button type="button" class="btn btn-sm btn-link p-0 mx-1"
|
||||
onclick="showAttorneyPopup('${attorney}', this)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>`;
|
||||
signatureTypeHtml = `<span class="badge bg-warning text-dark">МЧД</span>${eyeIcon}`;
|
||||
const snils = (practitioner.snils || '').replace(/"/g, '"').replace(/'/g, ''');
|
||||
const snilsIcon = `<button type="button" class="btn btn-sm btn-link p-0 ms-1"
|
||||
onclick="showAttorneyPopup('${snils}', this)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>`;
|
||||
signatureTypeHtml = `<span class="badge bg-warning text-dark">МЧД</span>${eyeIcon}
|
||||
<span class="badge bg-info">СНИЛС</span>${snilsIcon}`;
|
||||
}
|
||||
|
||||
// Форматируем дату и проверяем срок
|
||||
@@ -630,6 +636,7 @@ function editPractitioner(userIdLpu, isEsiaAuth) {
|
||||
// Редактирование УКЭП
|
||||
document.getElementById('ukepPractitionerId').value = userIdLpu;
|
||||
document.getElementById('ukepFile').required = false;
|
||||
document.getElementById('ukepSnilsNumber').value = practitioner.snils;
|
||||
|
||||
// Очищаем data-атрибуты модального окна
|
||||
const modal = document.getElementById('editUKEPModal');
|
||||
@@ -650,6 +657,7 @@ function editPractitioner(userIdLpu, isEsiaAuth) {
|
||||
// Редактирование МЧД
|
||||
document.getElementById('mchdPractitionerId').value = userIdLpu;
|
||||
document.getElementById('mchdNumber').value = practitioner.attorney || '';
|
||||
document.getElementById('mchdSnilsNumber').value = practitioner.snils;
|
||||
|
||||
// Очищаем data-атрибуты модального окна
|
||||
const modal = document.getElementById('editMCHDModal');
|
||||
@@ -779,6 +787,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const userIdLpu = document.getElementById('mchdPractitionerId').value;
|
||||
const mchdNumber = document.getElementById('mchdNumber').value.trim();
|
||||
const expiryDate = document.getElementById('mchdExpiryDate').value;
|
||||
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, '');
|
||||
|
||||
if (!mchdNumber || !expiryDate) {
|
||||
showAlert('Заполните все обязательные поля', 'warning');
|
||||
@@ -813,6 +822,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
userIdLpu: userIdLpu,
|
||||
esiaAuth: false,
|
||||
attorney: mchdNumber,
|
||||
snils: snils,
|
||||
expired_at: expiryDate
|
||||
};
|
||||
|
||||
@@ -861,6 +871,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
requestBody = {
|
||||
userIdLpu: userIdLpu,
|
||||
attorney: mchdNumber,
|
||||
snils: snils,
|
||||
expired_at: expiryDate
|
||||
};
|
||||
|
||||
@@ -903,6 +914,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const userIdLpu = document.getElementById('ukepPractitionerId').value;
|
||||
const ukepFile = document.getElementById('ukepFile').files[0];
|
||||
const expiryDate = document.getElementById('ukepExpiryDate').value;
|
||||
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, '');
|
||||
|
||||
if (!expiryDate) {
|
||||
showAlert('Заполните дату окончания действия', 'warning');
|
||||
@@ -942,6 +954,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
userIdLpu: userIdLpu,
|
||||
esiaAuth: true,
|
||||
attorney: '',
|
||||
snils: snils,
|
||||
expired_at: expiryDate
|
||||
};
|
||||
|
||||
@@ -991,6 +1004,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
const updateData = {
|
||||
userIdLpu: userIdLpu,
|
||||
snils: snils,
|
||||
expired_at: expiryDate
|
||||
};
|
||||
|
||||
|
||||
@@ -18,21 +18,21 @@
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<!-- Карточка настройки пароля -->
|
||||
<!-- Карточка способов доставки сообщений -->
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 border rounded h-100">
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="bi bi-shield-lock fs-4 text-primary"></i>
|
||||
<i class="bi bi-chat-dots fs-4 text-primary"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="mb-1">Пароль администратора</h6>
|
||||
<span id="passwordStatusBadge" class="badge bg-success">Настроен</span>
|
||||
<h6 class="mb-1">Каналы доставки</h6>
|
||||
<span id="deliveryStatusBadge" class="badge bg-secondary">Не настроены</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small mb-0">
|
||||
Пароль администратора установлен и защищает доступ к настройкам системы.
|
||||
</p>
|
||||
<div id="deliveryDetails" class="small">
|
||||
<!-- Динамически заполняемые детали -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -57,6 +57,120 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок способов доставки -->
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#deliverySection">
|
||||
<div class="d-flex align-items-center w-100">
|
||||
<span class="me-3">Способы доставки</span>
|
||||
<span id="deliveryStatus" class="badge bg-secondary">Загрузка...</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="deliverySection" class="accordion-collapse collapse" data-bs-parent="#settingsAccordion">
|
||||
<div class="accordion-body">
|
||||
<form id="deliveryForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Доступные способы доставки (минимум один должен быть
|
||||
выбран)</label>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||
id="deliveryMax" value="max">
|
||||
<label class="form-check-label" for="deliveryMax">
|
||||
MAX
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||
id="deliverySms" value="sms">
|
||||
<label class="form-check-label" for="deliverySms">
|
||||
SMS
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||
id="deliveryMila" value="mila">
|
||||
<label class="form-check-label" for="deliveryMila">
|
||||
MILA
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input delivery-checkbox" type="checkbox"
|
||||
id="deliveryGoskey" value="goskey">
|
||||
<label class="form-check-label" for="deliveryGoskey">
|
||||
GOSKEY
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="deliveryValidationMessage" class="form-text text-danger mt-2"
|
||||
style="display: none;">
|
||||
Должен быть выбран хотя бы один способ доставки
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" id="saveDeliveryBtn" class="btn btn-primary" disabled>
|
||||
Сохранить настройки доставки
|
||||
</button>
|
||||
<button type="button" id="cancelDeliveryBtn" class="btn btn-outline-secondary" disabled>
|
||||
Отменить изменения
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- После блока способов доставки, перед блоком Медодс -->
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#sendingSection">
|
||||
<div class="d-flex align-items-center w-100">
|
||||
<span class="me-3">Множественная отправка</span>
|
||||
<span id="sendingStatus" class="badge bg-secondary">Загрузка...</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="sendingSection" class="accordion-collapse collapse" data-bs-parent="#settingsAccordion">
|
||||
<div class="accordion-body">
|
||||
<form id="sendingForm">
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="multipleSending" role="switch">
|
||||
<label class="form-check-label fw-bold" for="multipleSending">
|
||||
Отправка нескольким получателям
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text text-muted mt-2">
|
||||
Включение и отключение возможности отправлять на подписание одновременно нескольким
|
||||
получателям.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" id="saveSendingBtn" class="btn btn-primary" disabled>
|
||||
Сохранить настройки отправки
|
||||
</button>
|
||||
<button type="button" id="cancelSendingBtn" class="btn btn-outline-secondary" disabled>
|
||||
Отменить изменения
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блок Медодс -->
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
|
||||
@@ -164,6 +164,10 @@
|
||||
<label for="mchdExpiryDate" class="form-label">Дата окончания действия</label>
|
||||
<input type="date" class="form-control" id="mchdExpiryDate" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="mchdSnilsNumber" class="form-label">СНИЛС (только цифры)</label>
|
||||
<input type="text" class="form-control" id="mchdSnilsNumber" required>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -193,6 +197,10 @@
|
||||
<label for="ukepExpiryDate" class="form-label">Дата окончания действия</label>
|
||||
<input type="date" class="form-control" id="ukepExpiryDate" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ukepSnilsNumber" class="form-label">СНИЛС (только цифры)</label>
|
||||
<input type="text" class="form-control" id="ukepSnilsNumber" required>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
some_data
|
||||
@@ -66,7 +66,7 @@ class Patient(Base):
|
||||
f"{kwargs.get('familyName', '')} {kwargs.get('givenName', '')} {kwargs.get('middleName', '')}"
|
||||
)
|
||||
kwargs["birthDate"] = datetime.fromisoformat(kwargs.pop("birthDate"))
|
||||
utils.logger.info(
|
||||
utils.logger.debug(
|
||||
f"Добавление пациента {kwargs.get('name', '')} с идентификатором {idPatientMis}"
|
||||
)
|
||||
patient = Patient(**kwargs)
|
||||
|
||||
@@ -16,6 +16,7 @@ class Practitioner(Base):
|
||||
fullName = Column(String)
|
||||
esiaAuth = Column(Boolean)
|
||||
attorney = Column(String, nullable=True)
|
||||
snils = Column(String)
|
||||
expired_at = Column(DateTime)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
import secrets
|
||||
from sqlalchemy import Column, DateTime, Integer, String, select
|
||||
from sqlalchemy import Boolean, Column, DateTime, Integer, String, select
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from db import Base, CRUD
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerifyMismatchError
|
||||
@@ -23,7 +24,11 @@ class Settings(Base):
|
||||
n3healthHost = Column(String, nullable=True)
|
||||
n3healthAutorization = Column(String, nullable=True)
|
||||
n3healthXIdLpu = Column(String, nullable=True)
|
||||
expirationAlert = Column(Integer, nullable=True)
|
||||
expirationAlert = Column(Integer, default=45)
|
||||
deliveryType = Column(
|
||||
JSONB, default={"sms": True, "max": True, "mila": False, "goskey": False}
|
||||
)
|
||||
multipleSending = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
@@ -69,7 +74,6 @@ class Settings(Base):
|
||||
"n3healthHost": "https://b2b-demo.n3health.ru/sep/api",
|
||||
"n3healthAutorization": "42e2080c-e33f-0800-3080-4e6989e3d97f",
|
||||
"n3healthXIdLpu": "8fc38b60-035b-462d-ad1b-31863e0fd2f0",
|
||||
"expirationAlert": 45,
|
||||
}
|
||||
settings = Settings(**settingsData)
|
||||
data = await settings.save()
|
||||
|
||||
+56
-23
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func, select
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
from db import Base, CRUD
|
||||
from utils import logger, answer, toDict
|
||||
@@ -15,8 +16,10 @@ class Signing(Base):
|
||||
idPatientMis = Column(
|
||||
String, ForeignKey("patients.idPatientMis", ondelete="CASCADE")
|
||||
)
|
||||
storagePath = Column(String, nullable=True)
|
||||
recipients = Column(JSONB, default=[])
|
||||
storagePath = Column(JSONB, default=[])
|
||||
trackingId = Column(String, nullable=True)
|
||||
deliveryType = Column(String)
|
||||
documents = relationship(
|
||||
"Document",
|
||||
cascade="all, delete-orphan",
|
||||
@@ -51,7 +54,9 @@ class Signing(Base):
|
||||
return await CRUD.delete(self)
|
||||
|
||||
@staticmethod
|
||||
async def addSigning(userIdLpu: str, idPatientMis: str):
|
||||
async def addSigning(
|
||||
userIdLpu: str, idPatientMis: str, deliveryType: str, recipients: list = []
|
||||
):
|
||||
from utils.background import enable_job
|
||||
|
||||
if type(userIdLpu) != str:
|
||||
@@ -61,7 +66,12 @@ class Signing(Base):
|
||||
|
||||
enable_job()
|
||||
|
||||
return await Signing(userIdLpu=userIdLpu, idPatientMis=idPatientMis).save()
|
||||
return await Signing(
|
||||
userIdLpu=userIdLpu,
|
||||
idPatientMis=idPatientMis,
|
||||
deliveryType=deliveryType,
|
||||
recipients=recipients,
|
||||
).save()
|
||||
|
||||
@staticmethod
|
||||
async def getSigningById(id: int, toDict: bool = True):
|
||||
@@ -76,14 +86,36 @@ class Signing(Base):
|
||||
async def getSigningsByIdPatientMis(idPatientMis: str, toDict: bool = True):
|
||||
if type(idPatientMis) != str:
|
||||
idPatientMis = str(idPatientMis)
|
||||
|
||||
# Поиск подписаний, где idPatientMis является прямым значением поля idPatientMis
|
||||
signings = await CRUD.read(
|
||||
select(Signing).where(Signing.idPatientMis == idPatientMis), True
|
||||
select(Signing).where(Signing.idPatientMis == idPatientMis),
|
||||
True,
|
||||
)
|
||||
|
||||
data = []
|
||||
if signings:
|
||||
data = [signing.toDict() for signing in signings] if toDict else signings
|
||||
data.extend(
|
||||
[signing.toDict() for signing in signings] if toDict else signings
|
||||
)
|
||||
|
||||
signings2 = await CRUD.read(
|
||||
select(Signing).where(Signing.recipients.contains([idPatientMis])),
|
||||
True,
|
||||
)
|
||||
|
||||
# Добавляем только те записи из signings2, которых еще нет в data (чтобы избежать дубликатов)
|
||||
if signings2:
|
||||
existing_ids = {s.id for s in data if hasattr(s, "id")}
|
||||
for signing in signings2:
|
||||
if signing.id not in existing_ids:
|
||||
data.append(signing.toDict() if toDict else signing)
|
||||
|
||||
if len(data) > 0:
|
||||
return answer(data=data)
|
||||
logger.error(f"Подписания не найдены, idPatientMis: {idPatientMis}")
|
||||
return answer(success=False, message="Подписания не найдены")
|
||||
|
||||
logger.error(f"Подписания не найдены, idPatientMis: {idPatientMis}")
|
||||
return answer(success=False, message="Подписания не найдены")
|
||||
|
||||
@staticmethod
|
||||
async def getSigningsByUserIdLpu(userIdLpu: str, toDict: bool = True):
|
||||
@@ -116,21 +148,17 @@ class Signing(Base):
|
||||
|
||||
@staticmethod
|
||||
async def updateStatuses():
|
||||
checkList = []
|
||||
|
||||
singingsInProgress = await CRUD.read(
|
||||
select(Signing).where(Signing.storagePath == None), True
|
||||
select(Signing).where(
|
||||
func.jsonb_array_length(Signing.storagePath) == 0,
|
||||
Signing.trackingId != None,
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
if singingsInProgress and len(singingsInProgress) > 0:
|
||||
for signing in singingsInProgress:
|
||||
if len(signing.statuses) > 0:
|
||||
maxCode = max([status.status for status in signing.statuses])
|
||||
if maxCode >= 204:
|
||||
continue
|
||||
checkList.append(signing.id)
|
||||
checkList = [singing.id for singing in singingsInProgress]
|
||||
|
||||
logger.info(f"Проверка статусов для {len(checkList)} подписаний")
|
||||
logger.info(f"Проверка статусов для {len(checkList)} подписаний")
|
||||
|
||||
if len(checkList) > 0:
|
||||
from db.schemas.statuses import Statuses
|
||||
@@ -198,8 +226,13 @@ class Signing(Base):
|
||||
case _:
|
||||
pass
|
||||
|
||||
deliveryType = filters.get("deliveryType")
|
||||
|
||||
query = select(Signing)
|
||||
|
||||
if deliveryType is not None and deliveryType != "all":
|
||||
query = query.where(Signing.deliveryType == deliveryType)
|
||||
|
||||
if userFilter is not None:
|
||||
if userFilter: # True - только свои
|
||||
query = query.where(Signing.userIdLpu == userIdLpu)
|
||||
@@ -223,17 +256,17 @@ class Signing(Base):
|
||||
result = [
|
||||
signing
|
||||
for signing in result
|
||||
if (signing.storagePath is None and signing.trackingId is not None)
|
||||
if (
|
||||
len(signing.storagePath) == 0 and signing.trackingId is not None
|
||||
)
|
||||
]
|
||||
case "completed":
|
||||
result = [
|
||||
signing for signing in result if signing.storagePath is not None
|
||||
]
|
||||
result = [signing for signing in result if len(signing.storagePath) > 0]
|
||||
case "error":
|
||||
result = [
|
||||
signing
|
||||
for signing in result
|
||||
if (signing.trackingId is None and signing.storagePath is None)
|
||||
if (signing.trackingId is None and len(signing.storagePath) == 0)
|
||||
]
|
||||
case _:
|
||||
pass
|
||||
|
||||
+83
-28
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, select
|
||||
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, select
|
||||
from sqlalchemy.orm import relationship
|
||||
from db import CRUD, Base
|
||||
from db.schemas.signings import Signing
|
||||
from utils import answer, logger, toDict
|
||||
@@ -11,6 +11,18 @@ class Statuses(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
singingId = Column(Integer, ForeignKey("signings.id", ondelete="CASCADE"))
|
||||
idPatientMis = Column(
|
||||
String,
|
||||
ForeignKey("patients.idPatientMis", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
)
|
||||
patient = relationship(
|
||||
"Patient",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="joined",
|
||||
uselist=False,
|
||||
single_parent=True,
|
||||
)
|
||||
status = Column(Integer)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
@@ -28,12 +40,16 @@ class Statuses(Base):
|
||||
return await CRUD.update(Statuses, self.id, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
async def addStatus(singingId: int, status: int):
|
||||
async def addStatus(singingId: int, status: int, idPatientMis: str = None):
|
||||
if type(singingId) != int:
|
||||
singingId = int(singingId)
|
||||
if type(status) != int:
|
||||
status = int(status)
|
||||
await CRUD.create(Statuses(singingId=singingId, status=status))
|
||||
if idPatientMis is not None and type(idPatientMis) != str:
|
||||
idPatientMis = str(idPatientMis)
|
||||
await CRUD.create(
|
||||
Statuses(singingId=singingId, status=status, idPatientMis=idPatientMis)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def getStatus(singingId: int) -> list:
|
||||
@@ -58,36 +74,75 @@ class Statuses(Base):
|
||||
logger.error(singingData.message)
|
||||
return singingData
|
||||
|
||||
statusDB = await Statuses.getStatus(singingId)
|
||||
latestStatusCode = 0
|
||||
try:
|
||||
latestStatusCode = statusDB[-1].status
|
||||
except IndexError:
|
||||
pass
|
||||
statusesDB = await Statuses.getStatus(singingId)
|
||||
|
||||
statusData = await getTrackingIDSigning(
|
||||
singingData.data.trackingId, latestStatusCode, singingId
|
||||
)
|
||||
if not statusData.success:
|
||||
latestStatusData = {}
|
||||
|
||||
if len(statusesDB) > 0:
|
||||
for statusDB in statusesDB:
|
||||
idPatientMis = statusDB.idPatientMis
|
||||
if not idPatientMis:
|
||||
idPatientMis = "global"
|
||||
if idPatientMis not in latestStatusData:
|
||||
latestStatusData[idPatientMis] = 0
|
||||
if statusDB.status > latestStatusData[idPatientMis]:
|
||||
latestStatusData[idPatientMis] = statusDB.status
|
||||
|
||||
statusData = await getTrackingIDSigning(singingData.data.trackingId, singingId)
|
||||
if not statusData.success and not isNew:
|
||||
logger.error(statusData.message)
|
||||
return statusData
|
||||
|
||||
actualStatusCode = int(statusData.data["code"])
|
||||
if actualStatusCode == 0 and isNew:
|
||||
actualStatusCode = 201
|
||||
actualStatusData = {}
|
||||
|
||||
if latestStatusCode != actualStatusCode:
|
||||
logger.info(f"✔️ Обновился статус: {singingId} - {actualStatusCode}")
|
||||
await Statuses.addStatus(singingId, actualStatusCode)
|
||||
else:
|
||||
logger.info(f"📌 Статус не изменился: {singingId} - {actualStatusCode}")
|
||||
if statusData.data:
|
||||
actualStatusData = statusData.data
|
||||
elif isNew:
|
||||
actualStatusData = {"global": 207, singingData.data.idPatientMis: 201}
|
||||
for recipient in singingData.data.recipients:
|
||||
actualStatusData[recipient] = 201
|
||||
|
||||
if actualStatusCode == 204:
|
||||
logger.info(f"✅ Подписание завершено: {singingId}")
|
||||
await singingData.data.edit(storagePath=statusData.data["storagePath"])
|
||||
if not actualStatusData:
|
||||
logger.error("Нет данных о статусе")
|
||||
return answer()
|
||||
|
||||
if actualStatusCode > 204:
|
||||
logger.info(f"❌ Подписание не успешно: {statusData.data['description']}")
|
||||
await singingData.data.edit(trackingId=None)
|
||||
for idPatientMis, actualStatusCode in actualStatusData.items():
|
||||
if idPatientMis == "storagePath":
|
||||
continue
|
||||
latestStatusCode = latestStatusData.get(idPatientMis, 0)
|
||||
|
||||
if latestStatusCode != actualStatusCode:
|
||||
logger.info(
|
||||
f"⚠️ Обновился статус подписания #{singingId} для '{idPatientMis}': {actualStatusCode}"
|
||||
)
|
||||
await Statuses.addStatus(
|
||||
singingId,
|
||||
actualStatusCode,
|
||||
idPatientMis if idPatientMis != "global" else None,
|
||||
)
|
||||
|
||||
if (
|
||||
"storagePath" in actualStatusData
|
||||
and len(actualStatusData["storagePath"]) > 0
|
||||
):
|
||||
logger.info(f"✅ Подписание #{singingId} завершено успешно")
|
||||
await singingData.data.edit(
|
||||
storagePath=actualStatusData["storagePath"]
|
||||
)
|
||||
|
||||
elif actualStatusCode in [213, 498, 499, 500, 501]:
|
||||
logger.warning(
|
||||
f"❌ Подписание #{singingId} завершено не успешно: {actualStatusCode}"
|
||||
)
|
||||
await singingData.data.edit(trackingId=None)
|
||||
|
||||
elif actualStatusCode in [205, 206, 212]:
|
||||
logger.warning(
|
||||
f"❌ Подписание #{singingId} завершено не успешно для '{idPatientMis}': {actualStatusCode}"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"📌 Не обновился статус подписания #{singingId} для '{idPatientMis}': {latestStatusCode}"
|
||||
)
|
||||
|
||||
return answer()
|
||||
|
||||
@@ -61,11 +61,11 @@ async def requestPOST(
|
||||
async with session.post(
|
||||
url, json=json, headers=headers, **kwargs
|
||||
) as response:
|
||||
try:
|
||||
responseJson = await response.json()
|
||||
logger.warning(responseJson)
|
||||
except:
|
||||
logger.error("Не удалось распарсить ответ сервера")
|
||||
# try:
|
||||
# responseJson = await response.json()
|
||||
# logger.warning(responseJson)
|
||||
# except:
|
||||
# logger.error("Не удалось распарсить ответ сервера")
|
||||
response.raise_for_status()
|
||||
try:
|
||||
return await response.json()
|
||||
|
||||
@@ -36,7 +36,7 @@ def detect_and_save_attachment(
|
||||
return None
|
||||
|
||||
mime_type = "application/octet-stream"
|
||||
extension = ".bin"
|
||||
extension = ".sig"
|
||||
|
||||
for signature, detected_mime, detected_ext in ATTACHMENT_SIGNATURES:
|
||||
if raw.startswith(signature):
|
||||
|
||||
+23
-77
@@ -13,11 +13,10 @@ from utils import logger
|
||||
|
||||
|
||||
class WeasyPrintConverter:
|
||||
def __init__(self, output_dirs: list[str]):
|
||||
self.output_dirs = [Path(d).resolve() for d in output_dirs]
|
||||
def __init__(self, output_dir: list[str]):
|
||||
self.output_dir = Path(output_dir).resolve()
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.max_files = 5
|
||||
for output_dir in self.output_dirs:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.weasyprint_path = self._find_weasyprint()
|
||||
|
||||
@@ -428,7 +427,7 @@ class WeasyPrintConverter:
|
||||
</html>"""
|
||||
|
||||
def convert_html_to_pdf(
|
||||
self, html_content: str, output_paths: list[Path]
|
||||
self, html_content: str, output_path: Path
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
Конвертирует HTML в PDF и сохраняет во все указанные папки
|
||||
@@ -447,24 +446,15 @@ class WeasyPrintConverter:
|
||||
temp_html = f.name
|
||||
|
||||
try:
|
||||
# Создаем временный PDF
|
||||
with tempfile.NamedTemporaryFile(
|
||||
suffix=".pdf", delete=False
|
||||
) as temp_pdf_file:
|
||||
temp_pdf = temp_pdf_file.name
|
||||
|
||||
# Конвертируем во временный файл
|
||||
cmd = [
|
||||
self.weasyprint_path,
|
||||
temp_html,
|
||||
temp_pdf,
|
||||
output_path,
|
||||
"--encoding",
|
||||
"utf-8",
|
||||
"--presentational-hints",
|
||||
]
|
||||
|
||||
logger.debug("Выполняем однократную конвертацию WeasyPrint")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=180
|
||||
)
|
||||
@@ -475,54 +465,14 @@ class WeasyPrintConverter:
|
||||
)
|
||||
return False, f"Ошибка конвертации: {error_msg}"
|
||||
|
||||
# Проверяем временный PDF
|
||||
temp_pdf_path = Path(temp_pdf)
|
||||
if not temp_pdf_path.exists() or temp_pdf_path.stat().st_size < 1024:
|
||||
if not output_path.exists() or output_path.stat().st_size < 1024:
|
||||
return False, "Созданный PDF слишком мал или отсутствует"
|
||||
|
||||
# Копируем во все целевые папки
|
||||
successful_files = []
|
||||
failed_files = []
|
||||
|
||||
for output_path in output_paths:
|
||||
try:
|
||||
# Создаем родительскую папку
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Копируем файл
|
||||
import shutil
|
||||
|
||||
shutil.copy2(temp_pdf, output_path)
|
||||
|
||||
# Проверяем результат
|
||||
if output_path.exists() and output_path.stat().st_size > 1024:
|
||||
successful_files.append(str(output_path))
|
||||
logger.debug(f"✅ PDF сохранен: {output_path}")
|
||||
else:
|
||||
failed_files.append(
|
||||
f"{output_path}: файл поврежден или слишком мал"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
failed_files.append(f"{output_path}: {str(e)}")
|
||||
logger.error(f"❌ Ошибка копирования в {output_path}: {str(e)}")
|
||||
|
||||
# Удаляем временный PDF
|
||||
os.unlink(temp_pdf)
|
||||
|
||||
# Формируем результат
|
||||
if successful_files and not failed_files:
|
||||
return (
|
||||
True,
|
||||
f"✅ PDF создан и сохранен во все {len(successful_files)} папок",
|
||||
)
|
||||
elif successful_files:
|
||||
return (
|
||||
True,
|
||||
f"⚠️ PDF создан, но сохранен только в {len(successful_files)}/{len(output_paths)} папок",
|
||||
)
|
||||
else:
|
||||
return False, f"❌ Не удалось сохранить PDF ни в одну папку"
|
||||
return (
|
||||
True,
|
||||
f"✅ PDF создан и сохранен: {output_path}",
|
||||
)
|
||||
|
||||
finally:
|
||||
if os.path.exists(temp_html):
|
||||
@@ -537,7 +487,7 @@ class WeasyPrintConverter:
|
||||
def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]:
|
||||
results = {
|
||||
"status": "SUCCESS",
|
||||
"files": {},
|
||||
"files": [],
|
||||
}
|
||||
|
||||
if len(docs) > self.max_files:
|
||||
@@ -553,19 +503,15 @@ class WeasyPrintConverter:
|
||||
|
||||
safe_title = self._clean_filename(doc_title)
|
||||
filename = f"{doc_number}_{safe_title}.pdf"
|
||||
output_paths = [output_dir / filename for output_dir in self.output_dirs]
|
||||
output_path = self.output_dir / filename
|
||||
|
||||
for path in self.output_dirs:
|
||||
key = path.parts[-1]
|
||||
if key not in results["files"]:
|
||||
results["files"][key] = []
|
||||
results["files"][key].append(
|
||||
{
|
||||
"number": int(doc_number),
|
||||
"title": doc_title,
|
||||
"storagePath": str(path / filename),
|
||||
}
|
||||
)
|
||||
results["files"].append(
|
||||
{
|
||||
"number": int(doc_number),
|
||||
"title": doc_title,
|
||||
"storagePath": str(output_path),
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"📄 Документ {i}/{len(docs)}: {filename}")
|
||||
|
||||
@@ -574,7 +520,7 @@ class WeasyPrintConverter:
|
||||
logger.warning(f" ⚠ Пропущен: недостаточно контента")
|
||||
continue
|
||||
|
||||
success, message = self.convert_html_to_pdf(html_content, output_paths)
|
||||
success, message = self.convert_html_to_pdf(html_content, output_path)
|
||||
|
||||
if success:
|
||||
logger.info(f" ✅ Успешно")
|
||||
@@ -584,11 +530,11 @@ class WeasyPrintConverter:
|
||||
return results
|
||||
|
||||
|
||||
def convert_docs_to_pdfs(docs: List[Dict], patientsData: list[dict]) -> Dict:
|
||||
def convert_docs_to_pdfs(docs: List[Dict], idPatientMis: str) -> Dict:
|
||||
try:
|
||||
baseDir = "src/attachments/export/"
|
||||
outputDirs = [f"{baseDir}{patient['idPatientMis']}" for patient in patientsData]
|
||||
converter = WeasyPrintConverter(outputDirs)
|
||||
outputDir = f"{baseDir}{idPatientMis}"
|
||||
converter = WeasyPrintConverter(outputDir)
|
||||
return converter.convert_documents(docs)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user