release 1.1

This commit is contained in:
2026-02-22 09:42:44 +03:00
parent be6cfe2284
commit 651631b3c7
35 changed files with 944 additions and 278 deletions
Vendored
BIN
View File
Binary file not shown.
+1
View File
@@ -174,3 +174,4 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
.DS_Store
+18
View File
@@ -69,3 +69,21 @@ async def getAllEmployees(host: str, port: int, identity: str, secret: str):
offset += limit offset += limit
return employees 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
View File
@@ -1,4 +1,7 @@
import config import asyncio
from datetime import datetime, timedelta
from functools import wraps
from db.schemas import Settings from db.schemas import Settings
from utils import ( from utils import (
logger, logger,
@@ -15,9 +18,48 @@ def getVerifySSL(url: str):
return "demo" not in url.lower() 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): async def sendForSigning(inData: dict):
settings = await Settings.getSettings(False) settings = await Settings.getSettings(False)
if not settings.success: if not settings.success:
@@ -29,9 +71,9 @@ async def sendForSigning(inData: dict):
"idLpu": settings.n3healthXIdLpu, "idLpu": settings.n3healthXIdLpu,
}, },
"practitioner": inData.get("practitioner", {}), "practitioner": inData.get("practitioner", {}),
"patients": inData.get("patients", {}), "patients": [inData.get("patient", {}), *inData.get("recipients", [])],
"medDocument": inData.get("medDocument", {}), "medDocument": inData.get("medDocument", {}),
"delivery": ["sms"], "delivery": [inData.get("deliveryType", "sms")],
} }
headers = { headers = {
@@ -49,7 +91,7 @@ async def sendForSigning(inData: dict):
return response 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) settings = await Settings.getSettings(False)
if not settings.success: if not settings.success:
logger.error("Настройки не инициализированы") logger.error("Настройки не инициализированы")
@@ -61,8 +103,8 @@ async def resend(trackingID: str, IdPatientMis: str):
} }
data = { data = {
"trackingId": trackingID, "trackingId": trackingID,
"IdPatientMis": [str(IdPatientMis)], "IdPatientMis": [str(IdPatientMis) for IdPatientMis in IdsPatientMis],
"delivery": ["sms"], "delivery": [deliveryType],
} }
response = await requestPOST( response = await requestPOST(
f"{settings.n3healthHost}/resend", f"{settings.n3healthHost}/resend",
@@ -108,32 +150,7 @@ async def checkConnection(host: str, authorization: str, xIdLpu: str):
) )
async def getAllSigning(): async def getTrackingIDSigning(trackingID: str, singingId: int):
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):
settings = await Settings.getSettings(False) settings = await Settings.getSettings(False)
if not settings.success: if not settings.success:
logger.error("Настройки не инициализированы") logger.error("Настройки не инициализированы")
@@ -154,24 +171,25 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId
patients = response.data.get("data", {}).get("patients", []) patients = response.data.get("data", {}).get("patients", [])
status = {"code": 0, "description": ""} statuses = {"global": int(response.data.get("data", {}).get("status", 0))}
patientsIds = []
for patient in patients: for patient in patients:
patientsIds.append(patient["idPatientMis"]) idPatientMis = patient.get("idPatientMis")
if patient.get("status", {}).get("code", 0) > status["code"]: if idPatientMis not in statuses:
status = patient.get("status", {}) 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 = ( attachments = (
response.data.get("data", {}).get("medDocument", {}).get("attachments", []) response.data.get("data", {}).get("medDocument", {}).get("attachments", [])
) )
if len(attachments) > 0: if len(attachments) > 0:
statuses["storagePath"] = []
for idx, attachment in enumerate(attachments, start=1): for idx, attachment in enumerate(attachments, start=1):
content = attachment.get("content") content = attachment.get("content")
@@ -179,20 +197,18 @@ async def getTrackingIDSigning(trackingID: str, latestStatusCode: int, singingId
continue continue
ATTACHMENTS_DIR = Path("src/attachments/import") 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( return answer(data=statuses, message=", ".join(messages))
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)
+231 -71
View File
@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from api.medods import findPatientByPhone
from api.n3health import resend, revoke, sendForSigning from api.n3health import resend, revoke, sendForSigning
from db.schemas.documents import Document from db.schemas.documents import Document
from db.schemas.patients import Patient from db.schemas.patients import Patient
@@ -37,9 +38,13 @@ async def pre_singing(
signature["expiration"] = practitioner.data.get("expired_at")[:10] signature["expiration"] = practitioner.data.get("expired_at")[:10]
daysRemainingControl = 0 daysRemainingControl = 0
deliveryType = None
settings = await Settings.getSettings() settings = await Settings.getSettings()
if settings.success: if settings.success:
daysRemainingControl = settings.data.get("expirationAlert", 0) 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 = [] inProgress = []
complete = [] complete = []
@@ -75,6 +80,8 @@ async def pre_singing(
"inProgress": inProgress, "inProgress": inProgress,
"complete": complete, "complete": complete,
"daysRemainingControl": daysRemainingControl, "daysRemainingControl": daysRemainingControl,
"deliveryType": deliveryType,
"multipleSending": multipleSending,
} }
return exitData return exitData
@@ -85,8 +92,9 @@ async def singing(
): ):
def correctData(data): def correctData(data):
data["practitioner"]["userIdLpu"] = str(data["practitioner"]["userIdLpu"]) data["practitioner"]["userIdLpu"] = str(data["practitioner"]["userIdLpu"])
for patient in data["patients"]: data["patient"]["idPatientMis"] = str(data["patient"]["idPatientMis"])
patient["idPatientMis"] = str(patient["idPatientMis"]) for recipient in data.get("recipients", []):
recipient["idPatientMis"] = str(recipient["idPatientMis"])
return data return data
logger.info(f"📥 Получен запрос на /singing") logger.info(f"📥 Получен запрос на /singing")
@@ -110,57 +118,109 @@ async def singing(
return {"status": "ERROR", "message": "Сотрудник не найден"} return {"status": "ERROR", "message": "Сотрудник не найден"}
esiaAuth = practitioner.data.get("esiaAuth", False) esiaAuth = practitioner.data.get("esiaAuth", False)
userIdLpu = practitioner.data.get("userIdLpu", 0) 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", []) patient = body.get("patient", None)
if len(patients) == 0: if not patient:
logger.error("Данные пациента не получены, пациент не найден") logger.error("Данные пациента не получены, пациент не найден")
return { return {
"status": "ERROR", "status": "ERROR",
"message": "Данные пациента не получены, пациент не найден", "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: recipients = body.pop("recipients", [])
idPatientMis = patient.get("idPatientMis", None)
if not idPatientMis: recipientsData = []
recipientsIds = []
for recipient in recipients:
idRecipientMis = recipient.get("idPatientMis", None)
if not idRecipientMis:
logger.error("Не указан идентификатор пациента") logger.error("Не указан идентификатор пациента")
return { return {
"status": "ERROR", "status": "ERROR",
"message": "Не указан идентификатор пациента", "message": "Не указан идентификатор пациента",
} }
patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True) recipientDB = await Patient.getPatientByIdPatientMis(
if not patientDB.success: idRecipientMis, isCheck=True
patientData = { )
"idPatientMis": idPatientMis, if not recipientDB.success:
"familyName": patient.get("familyName", "N/A"), recipientData = {
"givenName": patient.get("givenName", "N/A"), "idPatientMis": idRecipientMis,
"middleName": patient.get("middleName", "N/A"), "familyName": recipient.get("surname", "N/A"),
"birthDate": patient.get("birthDate", "N/A"), "givenName": recipient.get("name", "N/A"),
"sex": patient.get("sex", "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) newRecipient = await Patient.addPatient(**recipientData)
if not newPatient.success: if not newRecipient.success:
return { return {
"status": "ERROR", "status": "ERROR",
"message": newPatient.message, "message": newRecipient.message,
} }
patientsData.append(newPatient.data.toDict()) recipientsData.append(newRecipient.data.toDict())
recipientsIds.append(newRecipient.data.idPatientMis)
else: 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"👨‍⚕️ Медработник: {practitioner.data.get("name")}")
logger.info(f"👤 Пациент: {patientData.get('name')}")
logger.info( 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"📄 Документов для обработки: {len(docs)}")
logger.info(f"🔐 ЕСИА: {esiaAuth}") logger.info(f"🔐 ЕСИА: {esiaAuth}")
try: try:
# Конвертируем документы в PDF # Конвертируем документы в PDF
conversion_result = convert_docs_to_pdfs(docs, patientsData) conversion_result = convert_docs_to_pdfs(docs, idPatientMis)
# logger.info(conversion_result)
if conversion_result.get("status", "") != "SUCCESS": if conversion_result.get("status", "") != "SUCCESS":
return { return {
@@ -168,7 +228,9 @@ async def singing(
"message": "Не удалось сохранить документы", "message": "Не удалось сохранить документы",
} }
newSinging = await Signing.addSigning(userIdLpu, idPatientMis) newSinging = await Signing.addSigning(
userIdLpu, idPatientMis, body["deliveryType"], recipientsIds
)
if not newSinging: if not newSinging:
return { return {
"status": "ERROR", "status": "ERROR",
@@ -176,37 +238,41 @@ async def singing(
} }
logger.info(f"🔐 Подписание добавлено: {newSinging.id}") logger.info(f"🔐 Подписание добавлено: {newSinging.id}")
pdfFiles = conversion_result.get("files", {}) pdfFiles = conversion_result.get("files", [])
body["medDocument"] = { body["medDocument"] = {
"esiaAuth": esiaAuth, "esiaAuth": esiaAuth,
"attachments": [], "attachments": [],
} }
body["files"] = [] body["files"] = []
for key in pdfFiles: for doc in pdfFiles:
pdfFile = pdfFiles[key] for idPatient in [idPatientMis, *recipientsIds]:
for doc in pdfFile:
newDocument = await Document.addDocument( newDocument = await Document.addDocument(
singingId=newSinging.id, idPatientMis=key, **doc singingId=newSinging.id, idPatientMis=str(idPatient), **doc
) )
if not newDocument: if not newDocument:
logger.error(
f"Не удалось добавить документ для получателя {idPatient}"
)
return { return {
"status": "ERROR", "status": "ERROR",
"message": "Не удалось добавить документ", "message": "Не удалось добавить документ",
} }
logger.info(f"📄 Документ добавлен: {newDocument.id}") logger.debug(
partName = f'file{len(body["medDocument"]["attachments"])}' f"📄 Документ добавлен: {newDocument.id} для получателя {idPatient}"
body["medDocument"]["attachments"].append(
{
"partName": partName,
}
)
body["files"].append(
{
"field": partName,
"path": Path(doc["storagePath"]),
"content_type": "application/pdf",
}
) )
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: if esiaAuth:
partName = f'file{len(body["medDocument"]["attachments"])}' partName = f'file{len(body["medDocument"]["attachments"])}'
@@ -234,7 +300,7 @@ async def singing(
newSinging.trackingId = signingResult.get("trackingId", "") newSinging.trackingId = signingResult.get("trackingId", "")
await newSinging.save() await newSinging.save()
logger.info( logger.debug(
f"🔐 Подписание обновлено: {newSinging.id}. Добавлен Трек-номер подписания" f"🔐 Подписание обновлено: {newSinging.id}. Добавлен Трек-номер подписания"
) )
@@ -242,10 +308,9 @@ async def singing(
logger.info(f"✅ Обработка завершена!") logger.info(f"✅ Обработка завершена!")
# Возвращаем ответ
response = { response = {
"status": "SUCCESS", "status": "SUCCESS",
"message": f"Документы успешно обработаны. Трек-номер подписания: {newSinging.trackingId}", "message": "Документы успешно обработаны.",
} }
return response return response
@@ -274,6 +339,16 @@ async def statuses(
result = await Signing.getFilteredSingings(userIdLpu, filters) result = await Signing.getFilteredSingings(userIdLpu, filters)
singingsData = result.data 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 return singingsData
@@ -290,7 +365,7 @@ async def documents(
logger.error("Не отправлен idPatientMis") logger.error("Не отправлен idPatientMis")
return {"status": "ERROR", "message": "Не отправлен idPatientMis"} return {"status": "ERROR", "message": "Не отправлен idPatientMis"}
idPatientMis = body.get("idPatientMis") idPatientMis = str(body.get("idPatientMis"))
singingData = await Signing.getSigningsByIdPatientMis(idPatientMis) singingData = await Signing.getSigningsByIdPatientMis(idPatientMis)
@@ -308,7 +383,38 @@ async def documents(
for s in singingData.data: for s in singingData.data:
s["practitioner"] = practitioners[s.get("userIdLpu")] 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 return responseData
@@ -335,6 +441,7 @@ async def revoke_post(
reqData: dict = Depends(requestDict), reqData: dict = Depends(requestDict),
): ):
logger.info(f"📥 Получен запрос на /revoke") logger.info(f"📥 Получен запрос на /revoke")
trackingId = reqData.get("body", {}).get("trackingId", None) trackingId = reqData.get("body", {}).get("trackingId", None)
if not trackingId: if not trackingId:
return {"status": "ERROR", "message": "Документ не найден"} return {"status": "ERROR", "message": "Документ не найден"}
@@ -359,35 +466,25 @@ async def resend_post(
reqData: dict = Depends(requestDict), reqData: dict = Depends(requestDict),
): ):
logger.info(f"📥 Получен запрос на /resend") logger.info(f"📥 Получен запрос на /resend")
trackingId = reqData.get("body", {}).get("trackingId", None) trackingId = reqData.get("body", {}).get("trackingId", None)
idPatientMis = reqData.get("body", {}).get("idPatientMis", None) idPatientMis = reqData.get("body", {}).get("idPatientMis", None)
deliveryType = reqData.get("body", {}).get("deliveryType", None)
errorResponse = {"status": "ERROR", "message": "Повторная отправка не удалось"} errorResponse = {"status": "ERROR", "message": "Повторная отправка не удалось"}
if not trackingId or not idPatientMis: if not trackingId or not idPatientMis or not deliveryType:
logger.error("Не отправлен trackingId или idPatientMis") logger.error("Не отправлен trackingId или idPatientMis или deliveryType")
logger.error(f"trackingId: {trackingId}, idPatientMis: {idPatientMis}") logger.error(
f"trackingId: {trackingId}, idPatientMis: {idPatientMis}, deliveryType: {deliveryType}"
)
return errorResponse 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) logger.error(resendResult)
return errorResponse 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("✅ Повторная отправка успешна") logger.info("✅ Повторная отправка успешна")
return {"status": "SUCCESS", "message": resendResult.get("message", "")} return {"status": "SUCCESS", "message": resendResult.get("message", "")}
@@ -399,7 +496,70 @@ async def advanced_search_post(
): ):
logger.info(f"📥 Получен запрос на /advanced-search") logger.info(f"📥 Получен запрос на /advanced-search")
result = await Signing.getFilteredSingings( filters = reqData.get("body", {}).get("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} 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": [],
}
+25
View File
@@ -17,6 +17,7 @@ async def settingsPage(request: Request):
@router.get("/get", name="getSettings", summary="Получить настройки") @router.get("/get", name="getSettings", summary="Получить настройки")
async def getSettings(): async def getSettings():
settings = await Settings.getSettings() settings = await Settings.getSettings()
# logger.info(settings.data)
return settings.data return settings.data
@@ -33,6 +34,30 @@ async def updatePassword(reqData: dict = Depends(requestDict)):
return response 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="Проверить соединение с Медодс") @router.post("/test-medods", name="testMedods", summary="Проверить соединение с Медодс")
async def testMedods(reqData: dict = Depends(requestDict)): async def testMedods(reqData: dict = Depends(requestDict)):
logger.info(f"📥 Получен запрос на /test-medods") logger.info(f"📥 Получен запрос на /test-medods")
+56 -2
View File
@@ -2,7 +2,7 @@
let systemStatus = { let systemStatus = {
medods: { configured: false, details: {} }, medods: { configured: false, details: {} },
n3health: { configured: false, details: {} }, n3health: { configured: false, details: {} },
password: { configured: true } delivery: { configured: false, types: {} } // Добавлен новый объект для способов доставки
}; };
// Глобальные переменные для статистики сотрудников // Глобальные переменные для статистики сотрудников
@@ -44,6 +44,16 @@ async function loadSystemStatus() {
hasLpuId: !!data.n3healthXIdLpu 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 // Обновляем UI
updateStatusUI(); updateStatusUI();
updateReadinessIndicator(); updateReadinessIndicator();
@@ -227,6 +237,50 @@ function updateStatusUI() {
<div class="text-danger"><i class="bi bi-exclamation-circle"></i> Идентификатор МО: нет</div> <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'); const settingsButtonContainer = document.getElementById('settingsButtonContainer');
let configuredCount = 0; let configuredCount = 0;
if (systemStatus.password.configured) configuredCount++;
if (systemStatus.medods.configured) configuredCount++; if (systemStatus.medods.configured) configuredCount++;
if (systemStatus.n3health.configured) configuredCount++; if (systemStatus.n3health.configured) configuredCount++;
if (systemStatus.delivery.configured) configuredCount++; // Заменили password на delivery
const readinessPercentage = Math.round((configuredCount / 3) * 100); const readinessPercentage = Math.round((configuredCount / 3) * 100);
+219 -1
View File
@@ -9,12 +9,37 @@ let currentSettings = {
host: '', host: '',
authorization: '', authorization: '',
xIdLpu: '' xIdLpu: ''
} },
delivery: { // Добавьте эту секцию
max: false,
sms: false,
mila: false,
goskey: false
},
multipleSending: false
}; };
// Состояния изменений // Состояния изменений
let medodsChanged = false; let medodsChanged = false;
let n3healthChanged = 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') { 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() { function isMedodsFormValid() {
const host = document.getElementById('medodsHost').value; const host = document.getElementById('medodsHost').value;
@@ -94,6 +159,30 @@ function updateStatusIndicators() {
n3healthStatus.className = 'badge bg-danger'; n3healthStatus.className = 'badge bg-danger';
n3healthStatus.textContent = 'Требуется настройка'; 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; 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(); updateStatusIndicators();
} }
} catch (error) { } catch (error) {
@@ -264,6 +368,22 @@ function setupChangeListeners() {
updateN3HealthButtons(); 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 // Обработчик отмены изменений N3Health
document.getElementById('cancelN3HealthBtn').addEventListener('click', resetN3HealthForm); 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();
}); });
+16 -2
View File
@@ -368,11 +368,17 @@ function updateStaffTable() {
} else { } else {
// Экранируем специальные символы в attorney // Экранируем специальные символы в attorney
const attorney = (practitioner.attorney || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); const attorney = (practitioner.attorney || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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)"> onclick="showAttorneyPopup('${attorney}', this)">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</button>`; </button>`;
signatureTypeHtml = `<span class="badge bg-warning text-dark">МЧД</span>${eyeIcon}`; const snils = (practitioner.snils || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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('ukepPractitionerId').value = userIdLpu;
document.getElementById('ukepFile').required = false; document.getElementById('ukepFile').required = false;
document.getElementById('ukepSnilsNumber').value = practitioner.snils;
// Очищаем data-атрибуты модального окна // Очищаем data-атрибуты модального окна
const modal = document.getElementById('editUKEPModal'); const modal = document.getElementById('editUKEPModal');
@@ -650,6 +657,7 @@ function editPractitioner(userIdLpu, isEsiaAuth) {
// Редактирование МЧД // Редактирование МЧД
document.getElementById('mchdPractitionerId').value = userIdLpu; document.getElementById('mchdPractitionerId').value = userIdLpu;
document.getElementById('mchdNumber').value = practitioner.attorney || ''; document.getElementById('mchdNumber').value = practitioner.attorney || '';
document.getElementById('mchdSnilsNumber').value = practitioner.snils;
// Очищаем data-атрибуты модального окна // Очищаем data-атрибуты модального окна
const modal = document.getElementById('editMCHDModal'); const modal = document.getElementById('editMCHDModal');
@@ -779,6 +787,7 @@ document.addEventListener('DOMContentLoaded', function () {
const userIdLpu = document.getElementById('mchdPractitionerId').value; const userIdLpu = document.getElementById('mchdPractitionerId').value;
const mchdNumber = document.getElementById('mchdNumber').value.trim(); const mchdNumber = document.getElementById('mchdNumber').value.trim();
const expiryDate = document.getElementById('mchdExpiryDate').value; const expiryDate = document.getElementById('mchdExpiryDate').value;
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, '');
if (!mchdNumber || !expiryDate) { if (!mchdNumber || !expiryDate) {
showAlert('Заполните все обязательные поля', 'warning'); showAlert('Заполните все обязательные поля', 'warning');
@@ -813,6 +822,7 @@ document.addEventListener('DOMContentLoaded', function () {
userIdLpu: userIdLpu, userIdLpu: userIdLpu,
esiaAuth: false, esiaAuth: false,
attorney: mchdNumber, attorney: mchdNumber,
snils: snils,
expired_at: expiryDate expired_at: expiryDate
}; };
@@ -861,6 +871,7 @@ document.addEventListener('DOMContentLoaded', function () {
requestBody = { requestBody = {
userIdLpu: userIdLpu, userIdLpu: userIdLpu,
attorney: mchdNumber, attorney: mchdNumber,
snils: snils,
expired_at: expiryDate expired_at: expiryDate
}; };
@@ -903,6 +914,7 @@ document.addEventListener('DOMContentLoaded', function () {
const userIdLpu = document.getElementById('ukepPractitionerId').value; const userIdLpu = document.getElementById('ukepPractitionerId').value;
const ukepFile = document.getElementById('ukepFile').files[0]; const ukepFile = document.getElementById('ukepFile').files[0];
const expiryDate = document.getElementById('ukepExpiryDate').value; const expiryDate = document.getElementById('ukepExpiryDate').value;
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, '');
if (!expiryDate) { if (!expiryDate) {
showAlert('Заполните дату окончания действия', 'warning'); showAlert('Заполните дату окончания действия', 'warning');
@@ -942,6 +954,7 @@ document.addEventListener('DOMContentLoaded', function () {
userIdLpu: userIdLpu, userIdLpu: userIdLpu,
esiaAuth: true, esiaAuth: true,
attorney: '', attorney: '',
snils: snils,
expired_at: expiryDate expired_at: expiryDate
}; };
@@ -991,6 +1004,7 @@ document.addEventListener('DOMContentLoaded', function () {
const updateData = { const updateData = {
userIdLpu: userIdLpu, userIdLpu: userIdLpu,
snils: snils,
expired_at: expiryDate expired_at: expiryDate
}; };
+7 -7
View File
@@ -18,21 +18,21 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
<div class="row g-4"> <div class="row g-4">
<!-- Карточка настройки пароля --> <!-- Карточка способов доставки сообщений -->
<div class="col-md-4"> <div class="col-md-4">
<div class="p-3 border rounded h-100"> <div class="p-3 border rounded h-100">
<div class="d-flex align-items-start mb-3"> <div class="d-flex align-items-start mb-3">
<div class="flex-shrink-0"> <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>
<div class="flex-grow-1 ms-3"> <div class="flex-grow-1 ms-3">
<h6 class="mb-1">Пароль администратора</h6> <h6 class="mb-1">Каналы доставки</h6>
<span id="passwordStatusBadge" class="badge bg-success">Настроен</span> <span id="deliveryStatusBadge" class="badge bg-secondary">Не настроены</span>
</div> </div>
</div> </div>
<p class="text-muted small mb-0"> <div id="deliveryDetails" class="small">
Пароль администратора установлен и защищает доступ к настройкам системы. <!-- Динамически заполняемые детали -->
</p> </div>
</div> </div>
</div> </div>
+114
View File
@@ -57,6 +57,120 @@
</div> </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="#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"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
+8
View File
@@ -164,6 +164,10 @@
<label for="mchdExpiryDate" class="form-label">Дата окончания действия</label> <label for="mchdExpiryDate" class="form-label">Дата окончания действия</label>
<input type="date" class="form-control" id="mchdExpiryDate" required> <input type="date" class="form-control" id="mchdExpiryDate" required>
</div> </div>
<div class="mb-3">
<label for="mchdSnilsNumber" class="form-label">СНИЛС (только цифры)</label>
<input type="text" class="form-control" id="mchdSnilsNumber" required>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -193,6 +197,10 @@
<label for="ukepExpiryDate" class="form-label">Дата окончания действия</label> <label for="ukepExpiryDate" class="form-label">Дата окончания действия</label>
<input type="date" class="form-control" id="ukepExpiryDate" required> <input type="date" class="form-control" id="ukepExpiryDate" required>
</div> </div>
<div class="mb-3">
<label for="ukepSnilsNumber" class="form-label">СНИЛС (только цифры)</label>
<input type="text" class="form-control" id="ukepSnilsNumber" required>
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
BIN
View File
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.
Binary file not shown.
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
some_data
+1 -1
View File
@@ -66,7 +66,7 @@ class Patient(Base):
f"{kwargs.get('familyName', '')} {kwargs.get('givenName', '')} {kwargs.get('middleName', '')}" f"{kwargs.get('familyName', '')} {kwargs.get('givenName', '')} {kwargs.get('middleName', '')}"
) )
kwargs["birthDate"] = datetime.fromisoformat(kwargs.pop("birthDate")) kwargs["birthDate"] = datetime.fromisoformat(kwargs.pop("birthDate"))
utils.logger.info( utils.logger.debug(
f"Добавление пациента {kwargs.get('name', '')} с идентификатором {idPatientMis}" f"Добавление пациента {kwargs.get('name', '')} с идентификатором {idPatientMis}"
) )
patient = Patient(**kwargs) patient = Patient(**kwargs)
+1
View File
@@ -16,6 +16,7 @@ class Practitioner(Base):
fullName = Column(String) fullName = Column(String)
esiaAuth = Column(Boolean) esiaAuth = Column(Boolean)
attorney = Column(String, nullable=True) attorney = Column(String, nullable=True)
snils = Column(String)
expired_at = Column(DateTime) expired_at = Column(DateTime)
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
+7 -3
View File
@@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
import secrets 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 db import Base, CRUD
from argon2 import PasswordHasher from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError from argon2.exceptions import VerifyMismatchError
@@ -23,7 +24,11 @@ class Settings(Base):
n3healthHost = Column(String, nullable=True) n3healthHost = Column(String, nullable=True)
n3healthAutorization = Column(String, nullable=True) n3healthAutorization = Column(String, nullable=True)
n3healthXIdLpu = 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) created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=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", "n3healthHost": "https://b2b-demo.n3health.ru/sep/api",
"n3healthAutorization": "42e2080c-e33f-0800-3080-4e6989e3d97f", "n3healthAutorization": "42e2080c-e33f-0800-3080-4e6989e3d97f",
"n3healthXIdLpu": "8fc38b60-035b-462d-ad1b-31863e0fd2f0", "n3healthXIdLpu": "8fc38b60-035b-462d-ad1b-31863e0fd2f0",
"expirationAlert": 45,
} }
settings = Settings(**settingsData) settings = Settings(**settingsData)
data = await settings.save() data = await settings.save()
+56 -23
View File
@@ -1,5 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func, select from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from db import Base, CRUD from db import Base, CRUD
from utils import logger, answer, toDict from utils import logger, answer, toDict
@@ -15,8 +16,10 @@ class Signing(Base):
idPatientMis = Column( idPatientMis = Column(
String, ForeignKey("patients.idPatientMis", ondelete="CASCADE") String, ForeignKey("patients.idPatientMis", ondelete="CASCADE")
) )
storagePath = Column(String, nullable=True) recipients = Column(JSONB, default=[])
storagePath = Column(JSONB, default=[])
trackingId = Column(String, nullable=True) trackingId = Column(String, nullable=True)
deliveryType = Column(String)
documents = relationship( documents = relationship(
"Document", "Document",
cascade="all, delete-orphan", cascade="all, delete-orphan",
@@ -51,7 +54,9 @@ class Signing(Base):
return await CRUD.delete(self) return await CRUD.delete(self)
@staticmethod @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 from utils.background import enable_job
if type(userIdLpu) != str: if type(userIdLpu) != str:
@@ -61,7 +66,12 @@ class Signing(Base):
enable_job() enable_job()
return await Signing(userIdLpu=userIdLpu, idPatientMis=idPatientMis).save() return await Signing(
userIdLpu=userIdLpu,
idPatientMis=idPatientMis,
deliveryType=deliveryType,
recipients=recipients,
).save()
@staticmethod @staticmethod
async def getSigningById(id: int, toDict: bool = True): async def getSigningById(id: int, toDict: bool = True):
@@ -76,14 +86,36 @@ class Signing(Base):
async def getSigningsByIdPatientMis(idPatientMis: str, toDict: bool = True): async def getSigningsByIdPatientMis(idPatientMis: str, toDict: bool = True):
if type(idPatientMis) != str: if type(idPatientMis) != str:
idPatientMis = str(idPatientMis) idPatientMis = str(idPatientMis)
# Поиск подписаний, где idPatientMis является прямым значением поля idPatientMis
signings = await CRUD.read( signings = await CRUD.read(
select(Signing).where(Signing.idPatientMis == idPatientMis), True select(Signing).where(Signing.idPatientMis == idPatientMis),
True,
) )
data = []
if signings: 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) 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 @staticmethod
async def getSigningsByUserIdLpu(userIdLpu: str, toDict: bool = True): async def getSigningsByUserIdLpu(userIdLpu: str, toDict: bool = True):
@@ -116,21 +148,17 @@ class Signing(Base):
@staticmethod @staticmethod
async def updateStatuses(): async def updateStatuses():
checkList = []
singingsInProgress = await CRUD.read( 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: checkList = [singing.id for singing in singingsInProgress]
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)
logger.info(f"Проверка статусов для {len(checkList)} подписаний") logger.info(f"Проверка статусов для {len(checkList)} подписаний")
if len(checkList) > 0: if len(checkList) > 0:
from db.schemas.statuses import Statuses from db.schemas.statuses import Statuses
@@ -198,8 +226,13 @@ class Signing(Base):
case _: case _:
pass pass
deliveryType = filters.get("deliveryType")
query = select(Signing) query = select(Signing)
if deliveryType is not None and deliveryType != "all":
query = query.where(Signing.deliveryType == deliveryType)
if userFilter is not None: if userFilter is not None:
if userFilter: # True - только свои if userFilter: # True - только свои
query = query.where(Signing.userIdLpu == userIdLpu) query = query.where(Signing.userIdLpu == userIdLpu)
@@ -223,17 +256,17 @@ class Signing(Base):
result = [ result = [
signing signing
for signing in result 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": case "completed":
result = [ result = [signing for signing in result if len(signing.storagePath) > 0]
signing for signing in result if signing.storagePath is not None
]
case "error": case "error":
result = [ result = [
signing signing
for signing in result 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 _: case _:
pass pass
+83 -28
View File
@@ -1,6 +1,6 @@
from datetime import datetime 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 import CRUD, Base
from db.schemas.signings import Signing from db.schemas.signings import Signing
from utils import answer, logger, toDict from utils import answer, logger, toDict
@@ -11,6 +11,18 @@ class Statuses(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
singingId = Column(Integer, ForeignKey("signings.id", ondelete="CASCADE")) 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) status = Column(Integer)
created_at = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now)
@@ -28,12 +40,16 @@ class Statuses(Base):
return await CRUD.update(Statuses, self.id, **kwargs) return await CRUD.update(Statuses, self.id, **kwargs)
@staticmethod @staticmethod
async def addStatus(singingId: int, status: int): async def addStatus(singingId: int, status: int, idPatientMis: str = None):
if type(singingId) != int: if type(singingId) != int:
singingId = int(singingId) singingId = int(singingId)
if type(status) != int: if type(status) != int:
status = int(status) 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 @staticmethod
async def getStatus(singingId: int) -> list: async def getStatus(singingId: int) -> list:
@@ -58,36 +74,75 @@ class Statuses(Base):
logger.error(singingData.message) logger.error(singingData.message)
return singingData return singingData
statusDB = await Statuses.getStatus(singingId) statusesDB = await Statuses.getStatus(singingId)
latestStatusCode = 0
try:
latestStatusCode = statusDB[-1].status
except IndexError:
pass
statusData = await getTrackingIDSigning( latestStatusData = {}
singingData.data.trackingId, latestStatusCode, singingId
) if len(statusesDB) > 0:
if not statusData.success: 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) logger.error(statusData.message)
return statusData return statusData
actualStatusCode = int(statusData.data["code"]) actualStatusData = {}
if actualStatusCode == 0 and isNew:
actualStatusCode = 201
if latestStatusCode != actualStatusCode: if statusData.data:
logger.info(f"✔️ Обновился статус: {singingId} - {actualStatusCode}") actualStatusData = statusData.data
await Statuses.addStatus(singingId, actualStatusCode) elif isNew:
else: actualStatusData = {"global": 207, singingData.data.idPatientMis: 201}
logger.info(f"📌 Статус не изменился: {singingId} - {actualStatusCode}") for recipient in singingData.data.recipients:
actualStatusData[recipient] = 201
if actualStatusCode == 204: if not actualStatusData:
logger.info(f"✅ Подписание завершено: {singingId}") logger.error("Нет данных о статусе")
await singingData.data.edit(storagePath=statusData.data["storagePath"]) return answer()
if actualStatusCode > 204: for idPatientMis, actualStatusCode in actualStatusData.items():
logger.info(f"❌ Подписание не успешно: {statusData.data['description']}") if idPatientMis == "storagePath":
await singingData.data.edit(trackingId=None) 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() return answer()
+5 -5
View File
@@ -61,11 +61,11 @@ async def requestPOST(
async with session.post( async with session.post(
url, json=json, headers=headers, **kwargs url, json=json, headers=headers, **kwargs
) as response: ) as response:
try: # try:
responseJson = await response.json() # responseJson = await response.json()
logger.warning(responseJson) # logger.warning(responseJson)
except: # except:
logger.error("Не удалось распарсить ответ сервера") # logger.error("Не удалось распарсить ответ сервера")
response.raise_for_status() response.raise_for_status()
try: try:
return await response.json() return await response.json()
+1 -1
View File
@@ -36,7 +36,7 @@ def detect_and_save_attachment(
return None return None
mime_type = "application/octet-stream" mime_type = "application/octet-stream"
extension = ".bin" extension = ".sig"
for signature, detected_mime, detected_ext in ATTACHMENT_SIGNATURES: for signature, detected_mime, detected_ext in ATTACHMENT_SIGNATURES:
if raw.startswith(signature): if raw.startswith(signature):
+23 -77
View File
@@ -13,11 +13,10 @@ from utils import logger
class WeasyPrintConverter: class WeasyPrintConverter:
def __init__(self, output_dirs: list[str]): def __init__(self, output_dir: list[str]):
self.output_dirs = [Path(d).resolve() for d in output_dirs] self.output_dir = Path(output_dir).resolve()
self.output_dir.mkdir(parents=True, exist_ok=True)
self.max_files = 5 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() self.weasyprint_path = self._find_weasyprint()
@@ -428,7 +427,7 @@ class WeasyPrintConverter:
</html>""" </html>"""
def convert_html_to_pdf( def convert_html_to_pdf(
self, html_content: str, output_paths: list[Path] self, html_content: str, output_path: Path
) -> Tuple[bool, str]: ) -> Tuple[bool, str]:
""" """
Конвертирует HTML в PDF и сохраняет во все указанные папки Конвертирует HTML в PDF и сохраняет во все указанные папки
@@ -447,24 +446,15 @@ class WeasyPrintConverter:
temp_html = f.name temp_html = f.name
try: try:
# Создаем временный PDF
with tempfile.NamedTemporaryFile(
suffix=".pdf", delete=False
) as temp_pdf_file:
temp_pdf = temp_pdf_file.name
# Конвертируем во временный файл
cmd = [ cmd = [
self.weasyprint_path, self.weasyprint_path,
temp_html, temp_html,
temp_pdf, output_path,
"--encoding", "--encoding",
"utf-8", "utf-8",
"--presentational-hints", "--presentational-hints",
] ]
logger.debug("Выполняем однократную конвертацию WeasyPrint")
result = subprocess.run( result = subprocess.run(
cmd, capture_output=True, text=True, timeout=180 cmd, capture_output=True, text=True, timeout=180
) )
@@ -475,54 +465,14 @@ class WeasyPrintConverter:
) )
return False, f"Ошибка конвертации: {error_msg}" return False, f"Ошибка конвертации: {error_msg}"
# Проверяем временный PDF if not output_path.exists() or output_path.stat().st_size < 1024:
temp_pdf_path = Path(temp_pdf)
if not temp_pdf_path.exists() or temp_pdf_path.stat().st_size < 1024:
return False, "Созданный PDF слишком мал или отсутствует" 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 (
return ( True,
True, f"✅ PDF создан и сохранен: {output_path}",
f"✅ PDF создан и сохранен во все {len(successful_files)} папок", )
)
elif successful_files:
return (
True,
f"⚠️ PDF создан, но сохранен только в {len(successful_files)}/{len(output_paths)} папок",
)
else:
return False, f"❌ Не удалось сохранить PDF ни в одну папку"
finally: finally:
if os.path.exists(temp_html): if os.path.exists(temp_html):
@@ -537,7 +487,7 @@ class WeasyPrintConverter:
def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]: def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]:
results = { results = {
"status": "SUCCESS", "status": "SUCCESS",
"files": {}, "files": [],
} }
if len(docs) > self.max_files: if len(docs) > self.max_files:
@@ -553,19 +503,15 @@ class WeasyPrintConverter:
safe_title = self._clean_filename(doc_title) safe_title = self._clean_filename(doc_title)
filename = f"{doc_number}_{safe_title}.pdf" 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: results["files"].append(
key = path.parts[-1] {
if key not in results["files"]: "number": int(doc_number),
results["files"][key] = [] "title": doc_title,
results["files"][key].append( "storagePath": str(output_path),
{ }
"number": int(doc_number), )
"title": doc_title,
"storagePath": str(path / filename),
}
)
logger.info(f"📄 Документ {i}/{len(docs)}: {filename}") logger.info(f"📄 Документ {i}/{len(docs)}: {filename}")
@@ -574,7 +520,7 @@ class WeasyPrintConverter:
logger.warning(f" ⚠ Пропущен: недостаточно контента") logger.warning(f" ⚠ Пропущен: недостаточно контента")
continue 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: if success:
logger.info(f" ✅ Успешно") logger.info(f" ✅ Успешно")
@@ -584,11 +530,11 @@ class WeasyPrintConverter:
return results 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: try:
baseDir = "src/attachments/export/" baseDir = "src/attachments/export/"
outputDirs = [f"{baseDir}{patient['idPatientMis']}" for patient in patientsData] outputDir = f"{baseDir}{idPatientMis}"
converter = WeasyPrintConverter(outputDirs) converter = WeasyPrintConverter(outputDir)
return converter.convert_documents(docs) return converter.convert_documents(docs)
except Exception as e: except Exception as e: