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
.pypirc
.DS_Store
+18
View File
@@ -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,
)
+65 -49
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 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")
for patientId in patientsIds:
result = detect_and_save_attachment(
b64_content=content,
output_dir=ATTACHMENTS_DIR / patientId / str(singingId),
output_dir=ATTACHMENTS_DIR / str(singingId),
filename_prefix=f"document_{idx}",
save_attachments=True,
)
if not result:
logger.error("Файлы не сохранены")
message = "Файлы не сохранены"
message = f"Файл 'document_{idx}' не сохранены"
logger.error(message)
messages.append(message)
else:
status["storagePath"] = str(result["path"])
statuses["storagePath"].append(str(result["path"]))
return answer(data=status, message=message)
return answer(data=statuses, message=", ".join(messages))
+212 -52
View File
@@ -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,25 +118,22 @@ 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": "Данные пациента не получены, пациент не найден",
}
patientsData = []
for patient in patients:
idPatientMis = patient.get("idPatientMis", None)
if not idPatientMis:
logger.error("Не указан идентификатор пациента")
return {
"status": "ERROR",
"message": "Не указан идентификатор пациента",
}
return {"status": "ERROR", "message": "Не указан идентификатор пациента"}
patientDB = await Patient.getPatientByIdPatientMis(idPatientMis, isCheck=True)
if not patientDB.success:
patientData = {
@@ -143,24 +148,79 @@ async def singing(
if not newPatient.success:
return {
"status": "ERROR",
"message": newPatient.message,
"message": "Не удалось создать пациента",
}
patientsData.append(newPatient.data.toDict())
patientData = newPatient.data.toDict()
else:
patientsData.append(patientDB.data)
patientData = patientDB.data
recipients = body.pop("recipients", [])
recipientsData = []
recipientsIds = []
for recipient in recipients:
idRecipientMis = recipient.get("idPatientMis", None)
if not idRecipientMis:
logger.error("Не указан идентификатор пациента")
return {
"status": "ERROR",
"message": "Не указан идентификатор пациента",
}
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"),
}
newRecipient = await Patient.addPatient(**recipientData)
if not newRecipient.success:
return {
"status": "ERROR",
"message": newRecipient.message,
}
recipientsData.append(newRecipient.data.toDict())
recipientsIds.append(newRecipient.data.idPatientMis)
else:
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,24 +238,28 @@ 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}")
logger.debug(
f"📄 Документ добавлен: {newDocument.id} для получателя {idPatient}"
)
partName = f'file{len(body["medDocument"]["attachments"])}'
body["medDocument"]["attachments"].append(
{
@@ -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": [],
}
+25
View File
@@ -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")
+56 -2
View File
@@ -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);
+219 -1
View File
@@ -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();
});
+16 -2
View File
@@ -368,11 +368,17 @@ function updateStaffTable() {
} else {
// Экранируем специальные символы в attorney
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)">
<i class="bi bi-eye"></i>
</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('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
};
+7 -7
View File
@@ -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>
+114
View File
@@ -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">
+8
View File
@@ -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">
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', '')}"
)
kwargs["birthDate"] = datetime.fromisoformat(kwargs.pop("birthDate"))
utils.logger.info(
utils.logger.debug(
f"Добавление пациента {kwargs.get('name', '')} с идентификатором {idPatientMis}"
)
patient = Patient(**kwargs)
+1
View File
@@ -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)
+7 -3
View File
@@ -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()
+55 -22
View File
@@ -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,19 +148,15 @@ 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)} подписаний")
@@ -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
+81 -26
View File
@@ -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 statusData.data:
actualStatusData = statusData.data
elif isNew:
actualStatusData = {"global": 207, singingData.data.idPatientMis: 201}
for recipient in singingData.data.recipients:
actualStatusData[recipient] = 201
if not actualStatusData:
logger.error("Нет данных о статусе")
return answer()
for idPatientMis, actualStatusCode in actualStatusData.items():
if idPatientMis == "storagePath":
continue
latestStatusCode = latestStatusData.get(idPatientMis, 0)
if latestStatusCode != actualStatusCode:
logger.info(f"✔️ Обновился статус: {singingId} - {actualStatusCode}")
await Statuses.addStatus(singingId, actualStatusCode)
else:
logger.info(f"📌 Статус не изменился: {singingId} - {actualStatusCode}")
logger.info(
f"⚠️ Обновился статус подписания #{singingId} для '{idPatientMis}': {actualStatusCode}"
)
await Statuses.addStatus(
singingId,
actualStatusCode,
idPatientMis if idPatientMis != "global" else None,
)
if actualStatusCode == 204:
logger.info(f"✅ Подписание завершено: {singingId}")
await singingData.data.edit(storagePath=statusData.data["storagePath"])
if (
"storagePath" in actualStatusData
and len(actualStatusData["storagePath"]) > 0
):
logger.info(f"✅ Подписание #{singingId} завершено успешно")
await singingData.data.edit(
storagePath=actualStatusData["storagePath"]
)
if actualStatusCode > 204:
logger.info(f"❌ Подписание не успешно: {statusData.data['description']}")
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()
+5 -5
View File
@@ -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()
+1 -1
View File
@@ -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):
+15 -69
View File
@@ -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)} папок",
f"✅ PDF создан и сохранен: {output_path}",
)
elif successful_files:
return (
True,
f"⚠️ PDF создан, но сохранен только в {len(successful_files)}/{len(output_paths)} папок",
)
else:
return False, f"❌ Не удалось сохранить PDF ни в одну папку"
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,17 +503,13 @@ 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(
results["files"].append(
{
"number": int(doc_number),
"title": doc_title,
"storagePath": str(path / filename),
"storagePath": str(output_path),
}
)
@@ -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: