Upload 2 files
Browse files- app.py +519 -0
- requirements.txt +3 -0
app.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import openpyxl
|
| 3 |
+
from openpyxl.styles import PatternFill, Font
|
| 4 |
+
import re
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
|
| 10 |
+
DB_TK_FILE = "db_tk.xlsx"
|
| 11 |
+
DB_DINH_FILE = "db_dinh.xlsx"
|
| 12 |
+
DB_DVT_FILE = "db_dvt.xlsx"
|
| 13 |
+
DB_LIEU_FILE = "db_lieu.xlsx"
|
| 14 |
+
DB_NL_FILE = "db_nl.xlsx"
|
| 15 |
+
EXCEPTION_FILE = "exceptions.json"
|
| 16 |
+
|
| 17 |
+
def normalize(value):
|
| 18 |
+
if pd.isna(value):
|
| 19 |
+
return ""
|
| 20 |
+
return str(value).strip().replace(",", ".")
|
| 21 |
+
|
| 22 |
+
def normalize_unit(unit):
|
| 23 |
+
if pd.isna(unit):
|
| 24 |
+
return ""
|
| 25 |
+
return str(unit).strip().lower()
|
| 26 |
+
|
| 27 |
+
def normalize_drug_name(name):
|
| 28 |
+
return str(name).strip().lower() if pd.notna(name) else ""
|
| 29 |
+
|
| 30 |
+
def trim_column(df_input):
|
| 31 |
+
if "LIEU_DUNG" in df_input.columns:
|
| 32 |
+
df_input["LIEU_DUNG"] = df_input["LIEU_DUNG"].astype(str).str.strip()
|
| 33 |
+
if "DON_VI_TINH" in df_input.columns:
|
| 34 |
+
df_input["DON_VI_TINH"] = df_input["DON_VI_TINH"].astype(str).str.strip()
|
| 35 |
+
return df_input
|
| 36 |
+
|
| 37 |
+
def extract_units(lieu_dung_str):
|
| 38 |
+
text = str(lieu_dung_str).strip().lower()
|
| 39 |
+
m1 = re.match(r"^(\d+(?:[.,]\d+|\/\d+)?)[ ]*([^\s\/\[\]\*]+)", text)
|
| 40 |
+
unit1 = m1.group(2) if m1 else ""
|
| 41 |
+
m2 = re.search(r"\[\s*(\d+(?:[.,]\d+|\/\d+)?)\s*([^\s\/\[\]]+)", text)
|
| 42 |
+
unit2 = m2.group(2) if m2 else ""
|
| 43 |
+
return unit1.strip(), unit2.strip()
|
| 44 |
+
|
| 45 |
+
def get_special_format_set_from_file(file_path):
|
| 46 |
+
"""
|
| 47 |
+
Đọc file DB_DINH và trả về set các tuple (normalized_ten_thuoc, normalized_lieu_dung, normalized_so_luong).
|
| 48 |
+
"""
|
| 49 |
+
if not os.path.exists(file_path):
|
| 50 |
+
return set()
|
| 51 |
+
try:
|
| 52 |
+
df = pd.read_excel(file_path, header=0)
|
| 53 |
+
if not all(col in df.columns for col in ["TEN_THUOC", "LIEU_DUNG", "SO_LUONG"]):
|
| 54 |
+
return set()
|
| 55 |
+
exceptions = set()
|
| 56 |
+
for _, row in df.iterrows():
|
| 57 |
+
ten = normalize_drug_name(row["TEN_THUOC"])
|
| 58 |
+
lieu = normalize(row["LIEU_DUNG"])
|
| 59 |
+
so_luong = normalize(row["SO_LUONG"])
|
| 60 |
+
if ten and lieu and so_luong:
|
| 61 |
+
exceptions.add((ten, lieu, so_luong))
|
| 62 |
+
return exceptions
|
| 63 |
+
except Exception:
|
| 64 |
+
return set()
|
| 65 |
+
|
| 66 |
+
def get_special_qty_set_from_file(file_path):
|
| 67 |
+
"""
|
| 68 |
+
Đọc file DB_TK và trả về set các tuple (normalized_ten_thuoc, normalized_lieu_dung, normalized_so_luong).
|
| 69 |
+
"""
|
| 70 |
+
if not os.path.exists(file_path):
|
| 71 |
+
return set()
|
| 72 |
+
try:
|
| 73 |
+
df = pd.read_excel(file_path, header=0)
|
| 74 |
+
if not all(col in df.columns for col in ["TEN_THUOC", "LIEU_DUNG", "SO_LUONG"]):
|
| 75 |
+
return set()
|
| 76 |
+
exceptions = set()
|
| 77 |
+
for _, row in df.iterrows():
|
| 78 |
+
ten = normalize_drug_name(row["TEN_THUOC"])
|
| 79 |
+
lieu = normalize(row["LIEU_DUNG"])
|
| 80 |
+
so_luong = normalize(row["SO_LUONG"])
|
| 81 |
+
if ten and lieu and so_luong:
|
| 82 |
+
exceptions.add((ten, lieu, so_luong))
|
| 83 |
+
return exceptions
|
| 84 |
+
except Exception:
|
| 85 |
+
return set()
|
| 86 |
+
|
| 87 |
+
def get_special_unit_set_from_file(file_path):
|
| 88 |
+
"""
|
| 89 |
+
Đọc file DB_DVT và trả về set các tuple (normalized_ten_thuoc, normalized_lieu_dung, normalized_so_luong).
|
| 90 |
+
"""
|
| 91 |
+
if not os.path.exists(file_path):
|
| 92 |
+
return set()
|
| 93 |
+
try:
|
| 94 |
+
df = pd.read_excel(file_path, header=0)
|
| 95 |
+
if not all(col in df.columns for col in ["TEN_THUOC", "LIEU_DUNG", "SO_LUONG"]):
|
| 96 |
+
return set()
|
| 97 |
+
exceptions = set()
|
| 98 |
+
for _, row in df.iterrows():
|
| 99 |
+
ten = normalize_drug_name(row["TEN_THUOC"])
|
| 100 |
+
lieu = normalize(row["LIEU_DUNG"])
|
| 101 |
+
so_luong = normalize(row["SO_LUONG"])
|
| 102 |
+
if ten and lieu and so_luong:
|
| 103 |
+
exceptions.add((ten, lieu, so_luong))
|
| 104 |
+
return exceptions
|
| 105 |
+
except Exception:
|
| 106 |
+
return set()
|
| 107 |
+
|
| 108 |
+
def get_lieu_exceptions(file_path):
|
| 109 |
+
"""
|
| 110 |
+
Đọc file DB_LIEU và trả về set các tuple (normalized_ten_thuoc, normalized_lieu_dung, normalized_so_luong).
|
| 111 |
+
"""
|
| 112 |
+
if not os.path.exists(file_path):
|
| 113 |
+
return set()
|
| 114 |
+
try:
|
| 115 |
+
df = pd.read_excel(file_path, header=0)
|
| 116 |
+
if not all(col in df.columns for col in ["TEN_THUOC", "LIEU_DUNG", "SO_LUONG"]):
|
| 117 |
+
return set()
|
| 118 |
+
exceptions = set()
|
| 119 |
+
for _, row in df.iterrows():
|
| 120 |
+
ten = normalize_drug_name(row["TEN_THUOC"])
|
| 121 |
+
lieu = normalize(row["LIEU_DUNG"])
|
| 122 |
+
so_luong = normalize(row["SO_LUONG"])
|
| 123 |
+
if ten and lieu and so_luong:
|
| 124 |
+
exceptions.add((ten, lieu, so_luong))
|
| 125 |
+
return exceptions
|
| 126 |
+
except Exception:
|
| 127 |
+
return set()
|
| 128 |
+
|
| 129 |
+
def get_allowed_exceptions_tuples(file_path):
|
| 130 |
+
"""
|
| 131 |
+
Đọc file DB_NL và trả về set các tuple (normalized_ten_thuoc, normalized_lieu_dung, normalized_so_luong).
|
| 132 |
+
"""
|
| 133 |
+
if not os.path.exists(file_path):
|
| 134 |
+
return set()
|
| 135 |
+
try:
|
| 136 |
+
df = pd.read_excel(file_path, header=0)
|
| 137 |
+
if not all(col in df.columns for col in ["TEN_THUOC", "LIEU_DUNG", "SO_LUONG"]):
|
| 138 |
+
return set()
|
| 139 |
+
exceptions = set()
|
| 140 |
+
for _, row in df.iterrows():
|
| 141 |
+
ten = normalize_drug_name(row["TEN_THUOC"])
|
| 142 |
+
lieu = normalize(row["LIEU_DUNG"])
|
| 143 |
+
so_luong = normalize(row["SO_LUONG"])
|
| 144 |
+
if ten and lieu and so_luong:
|
| 145 |
+
exceptions.add((ten, lieu, so_luong))
|
| 146 |
+
return exceptions
|
| 147 |
+
except Exception:
|
| 148 |
+
return set()
|
| 149 |
+
|
| 150 |
+
def load_exceptions():
|
| 151 |
+
if not os.path.exists(EXCEPTION_FILE):
|
| 152 |
+
return {
|
| 153 |
+
"format": [],
|
| 154 |
+
"allowed": [],
|
| 155 |
+
"unitAllowed": []
|
| 156 |
+
}
|
| 157 |
+
with open(EXCEPTION_FILE, "r", encoding="utf-8") as f:
|
| 158 |
+
data = json.load(f)
|
| 159 |
+
return {
|
| 160 |
+
"format": [normalize(x) for x in data.get("formatErrors", [])],
|
| 161 |
+
"allowed": [normalize(x) for x in data.get("allowedExceptions", [])],
|
| 162 |
+
"unitAllowed": [normalize(x) for x in data.get("unitAllowed", [])]
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
def save_exceptions_from_files(db_tk_file, db_dinh_file, db_dvt_file, db_lieu_file, db_nl_file):
|
| 166 |
+
file_map = [
|
| 167 |
+
(db_tk_file, DB_TK_FILE),
|
| 168 |
+
(db_dinh_file, DB_DINH_FILE),
|
| 169 |
+
(db_dvt_file, DB_DVT_FILE),
|
| 170 |
+
(db_lieu_file, DB_LIEU_FILE),
|
| 171 |
+
(db_nl_file, DB_NL_FILE)
|
| 172 |
+
]
|
| 173 |
+
for fileobj, target in file_map:
|
| 174 |
+
if fileobj is not None:
|
| 175 |
+
try:
|
| 176 |
+
if hasattr(fileobj, "name"):
|
| 177 |
+
with open(fileobj.name, "rb") as src, open(target, "wb") as dst:
|
| 178 |
+
dst.write(src.read())
|
| 179 |
+
else:
|
| 180 |
+
fileobj.seek(0)
|
| 181 |
+
with open(target, "wb") as f:
|
| 182 |
+
f.write(fileobj.read())
|
| 183 |
+
except Exception as e:
|
| 184 |
+
return f"Lỗi lưu {target}: {e}"
|
| 185 |
+
|
| 186 |
+
def read_col_a(fileobj):
|
| 187 |
+
if fileobj is None:
|
| 188 |
+
return []
|
| 189 |
+
try:
|
| 190 |
+
if hasattr(fileobj, "name"):
|
| 191 |
+
df = pd.read_excel(fileobj.name, sheet_name=0, header=0)
|
| 192 |
+
else:
|
| 193 |
+
fileobj.seek(0)
|
| 194 |
+
df = pd.read_excel(fileobj, sheet_name=0, header=0)
|
| 195 |
+
return [str(x).strip() for x in df.iloc[:,0] if pd.notna(x) and str(x).strip() != ""]
|
| 196 |
+
except Exception:
|
| 197 |
+
return []
|
| 198 |
+
format_list = read_col_a(db_dinh_file)
|
| 199 |
+
allowed_list = [] # không dùng allowed_list kiểu cũ nữa
|
| 200 |
+
unitallowed_list = read_col_a(db_dvt_file)
|
| 201 |
+
if os.path.exists(EXCEPTION_FILE):
|
| 202 |
+
with open(EXCEPTION_FILE, "r", encoding="utf-8") as f:
|
| 203 |
+
data = json.load(f)
|
| 204 |
+
else:
|
| 205 |
+
data = {
|
| 206 |
+
"formatErrors": [],
|
| 207 |
+
"allowedExceptions": [],
|
| 208 |
+
"unitAllowed": []
|
| 209 |
+
}
|
| 210 |
+
if format_list:
|
| 211 |
+
data["formatErrors"] = format_list
|
| 212 |
+
if unitallowed_list:
|
| 213 |
+
data["unitAllowed"] = unitallowed_list
|
| 214 |
+
with open(EXCEPTION_FILE, "w", encoding="utf-8") as f:
|
| 215 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 216 |
+
|
| 217 |
+
return "✅ Đã cập nhật DB_TK, DB_DINH, DB_DVT, DB_LIEU, DB_NL!"
|
| 218 |
+
|
| 219 |
+
def highlight_insulin_errors(df_input, insulin_file):
|
| 220 |
+
if insulin_file is None:
|
| 221 |
+
return [], []
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
df_insulin = pd.read_excel(insulin_file.name if hasattr(insulin_file, "name") else insulin_file, header=0)
|
| 225 |
+
except Exception:
|
| 226 |
+
return [], []
|
| 227 |
+
|
| 228 |
+
insulin_map = {}
|
| 229 |
+
for idx, row in df_insulin.iterrows():
|
| 230 |
+
ten = normalize_drug_name(row.get("TEN_THUOC", ""))
|
| 231 |
+
try:
|
| 232 |
+
hamluong = float(str(row.get("HAM_LUONG", "")).replace(",", "."))
|
| 233 |
+
so_sanh = float(str(row.get("SO_LUONG_SO_SANH", "")).replace(",", "."))
|
| 234 |
+
except Exception:
|
| 235 |
+
continue
|
| 236 |
+
if ten and hamluong is not None and so_sanh is not None:
|
| 237 |
+
insulin_map[ten] = (hamluong, so_sanh)
|
| 238 |
+
|
| 239 |
+
error_rows_cam = []
|
| 240 |
+
error_rows_xanh = []
|
| 241 |
+
for idx, row in df_input.iterrows():
|
| 242 |
+
ten_thuoc = normalize_drug_name(row.get("TEN_THUOC", ""))
|
| 243 |
+
if ten_thuoc not in insulin_map:
|
| 244 |
+
continue
|
| 245 |
+
ham_luong, so_sanh = insulin_map[ten_thuoc]
|
| 246 |
+
lieu_goc = str(row.get("LIEU_DUNG", ""))
|
| 247 |
+
lieu = re.sub(r"\s+", "", lieu_goc)
|
| 248 |
+
matches = re.findall(r":(\d+)[iI][uU]\/lần", lieu)
|
| 249 |
+
tong_1_lan = sum(int(m) for m in matches) if matches else 0
|
| 250 |
+
match_ngay = re.search(r"\[(\d+)[iI][uU]\/ngày\]", lieu)
|
| 251 |
+
so_trong_ngay = int(match_ngay.group(1)) if match_ngay else 0
|
| 252 |
+
|
| 253 |
+
if tong_1_lan != so_trong_ngay or tong_1_lan == 0 or so_trong_ngay == 0:
|
| 254 |
+
error_rows_xanh.append(idx)
|
| 255 |
+
else:
|
| 256 |
+
match_son = re.search(r"\*(\d+)ngày", lieu)
|
| 257 |
+
so_ngay = int(match_son.group(1)) if match_son else 0
|
| 258 |
+
tong_chi_dinh = so_trong_ngay * so_ngay
|
| 259 |
+
so_luong_thuc_te = row.get("SO_LUONG", row.get("So_luong", row.get("so_luong", row.get("SỐ_LƯỢNG", row.get("M", None)))))
|
| 260 |
+
try:
|
| 261 |
+
so_luong_thuc_te = float(str(so_luong_thuc_te).replace(",", "."))
|
| 262 |
+
except Exception:
|
| 263 |
+
continue
|
| 264 |
+
tong_thuc_te = ham_luong * so_luong_thuc_te
|
| 265 |
+
hieu = tong_thuc_te - tong_chi_dinh
|
| 266 |
+
if hieu >= so_sanh:
|
| 267 |
+
error_rows_cam.append(idx)
|
| 268 |
+
return error_rows_cam, error_rows_xanh
|
| 269 |
+
|
| 270 |
+
def highlight_invalid_rows(df_input, exceptions, special_format_tuples, lieu_exceptions, insulin_xanh_rows=None, allowed_exceptions=None):
|
| 271 |
+
pattern = re.compile(
|
| 272 |
+
r"^(\d+(?:[.,]\d+|\/\d+)?)\s+"
|
| 273 |
+
r"(ml|mg|viên|gói|chai|lọ|ống|túi|giọt|iu|ui)\/lần\s*\*\s*"
|
| 274 |
+
r"(\d+)\s*lần\/ngày\s*\*\s*"
|
| 275 |
+
r"(\d+(?:[.,]\d+|\/\d+)?)\s*ngày\s*\["
|
| 276 |
+
r"(\d+(?:[.,]\d+|\/\d+)?)\s+"
|
| 277 |
+
r"(ml|mg|viên|gói|chai|lọ|ống|túi|giọt|iu|ui)\/ngày\]\.$"
|
| 278 |
+
)
|
| 279 |
+
dose_error_rows, format_error_rows = [], []
|
| 280 |
+
insulin_xanh_rows = set(insulin_xanh_rows) if insulin_xanh_rows else set()
|
| 281 |
+
allowed_exceptions = allowed_exceptions if allowed_exceptions is not None else set()
|
| 282 |
+
if "LIEU_DUNG" in df_input.columns:
|
| 283 |
+
for index, row in df_input.iterrows():
|
| 284 |
+
if index in insulin_xanh_rows:
|
| 285 |
+
dose_error_rows.append(index)
|
| 286 |
+
continue
|
| 287 |
+
ten_thuoc = normalize_drug_name(row.get("TEN_THUOC", row.get("ten_thuoc", "")))
|
| 288 |
+
lieu_value = normalize(row.get("LIEU_DUNG", ""))
|
| 289 |
+
so_luong = normalize(row.get("SO_LUONG", row.get("So_luong", row.get("so_luong", row.get("SỐ_LƯỢNG", row.get("M", None))))))
|
| 290 |
+
if (ten_thuoc, lieu_value, so_luong) in allowed_exceptions:
|
| 291 |
+
continue
|
| 292 |
+
if (ten_thuoc, lieu_value, so_luong) in lieu_exceptions:
|
| 293 |
+
continue
|
| 294 |
+
if (ten_thuoc, lieu_value, so_luong) in special_format_tuples:
|
| 295 |
+
continue
|
| 296 |
+
if lieu_value in exceptions.get("format", []):
|
| 297 |
+
format_error_rows.append(index)
|
| 298 |
+
continue
|
| 299 |
+
match = pattern.match(lieu_value)
|
| 300 |
+
if match:
|
| 301 |
+
try:
|
| 302 |
+
dose_per_time = float(match.group(1).replace(",", ".").replace("/", "/1.0"))
|
| 303 |
+
times_per_day = int(match.group(3))
|
| 304 |
+
actual_daily_dose = float(match.group(5).replace(",", ".").replace("/", "/1.0"))
|
| 305 |
+
expected = dose_per_time * times_per_day
|
| 306 |
+
if not math.isclose(expected, actual_daily_dose, rel_tol=1e-3):
|
| 307 |
+
dose_error_rows.append(index)
|
| 308 |
+
except Exception:
|
| 309 |
+
dose_error_rows.append(index)
|
| 310 |
+
else:
|
| 311 |
+
format_error_rows.append(index)
|
| 312 |
+
return dose_error_rows, format_error_rows
|
| 313 |
+
|
| 314 |
+
def highlight_unit_errors(df_input, exceptions, special_unit_tuples, allowed_exceptions=None):
|
| 315 |
+
unit_error_rows = []
|
| 316 |
+
allowed_unit_lieudung = set([normalize(x) for x in exceptions.get("unitAllowed", [])])
|
| 317 |
+
allowed_exceptions = allowed_exceptions if allowed_exceptions is not None else set()
|
| 318 |
+
for idx, row in df_input.iterrows():
|
| 319 |
+
ten_thuoc = normalize_drug_name(row.get("TEN_THUOC", row.get("ten_thuoc", "")))
|
| 320 |
+
lieu_dung_raw = row.get("LIEU_DUNG", "")
|
| 321 |
+
don_vi_tinh_raw = row.get("DON_VI_TINH", "")
|
| 322 |
+
lieu_dung_norm = normalize(lieu_dung_raw)
|
| 323 |
+
don_vi_tinh_norm = normalize_unit(don_vi_tinh_raw)
|
| 324 |
+
so_luong = normalize(row.get("SO_LUONG", row.get("So_luong", row.get("so_luong", row.get("SỐ_LƯỢNG", row.get("M", None))))))
|
| 325 |
+
if (ten_thuoc, lieu_dung_norm, so_luong) in allowed_exceptions:
|
| 326 |
+
continue
|
| 327 |
+
if (ten_thuoc, lieu_dung_norm, so_luong) in special_unit_tuples:
|
| 328 |
+
continue
|
| 329 |
+
if lieu_dung_norm in allowed_unit_lieudung:
|
| 330 |
+
continue
|
| 331 |
+
unit1, unit2 = extract_units(lieu_dung_raw)
|
| 332 |
+
unit1_norm = normalize_unit(unit1)
|
| 333 |
+
unit2_norm = normalize_unit(unit2)
|
| 334 |
+
if not don_vi_tinh_norm or don_vi_tinh_norm == "nan":
|
| 335 |
+
unit_error_rows.append(idx)
|
| 336 |
+
continue
|
| 337 |
+
if not (unit1_norm == don_vi_tinh_norm and unit2_norm == don_vi_tinh_norm):
|
| 338 |
+
unit_error_rows.append(idx)
|
| 339 |
+
return unit_error_rows
|
| 340 |
+
|
| 341 |
+
def highlight_wrong_quantity_rows(df_input, special_qty_tuples, allowed_exceptions=None):
|
| 342 |
+
wrong_qty_rows = []
|
| 343 |
+
allowed_exceptions = allowed_exceptions if allowed_exceptions is not None else set()
|
| 344 |
+
for idx, row in df_input.iterrows():
|
| 345 |
+
ten_thuoc = normalize_drug_name(row.get("TEN_THUOC", row.get("ten_thuoc", "")))
|
| 346 |
+
lieu_dung = str(row.get("LIEU_DUNG", ""))
|
| 347 |
+
lieu_dung_norm = normalize(lieu_dung)
|
| 348 |
+
so_luong = normalize(row.get("SO_LUONG", row.get("So_luong", row.get("so_luong", row.get("SỐ_LƯỢNG", row.get("M", None))))))
|
| 349 |
+
if (ten_thuoc, lieu_dung_norm, so_luong) in allowed_exceptions:
|
| 350 |
+
continue
|
| 351 |
+
if (ten_thuoc, lieu_dung_norm, so_luong) in special_qty_tuples:
|
| 352 |
+
continue
|
| 353 |
+
so_luong_thuc_te = row.get("SO_LUONG", row.get("So_luong", row.get("so_luong", row.get("SỐ_LƯỢNG", row.get("M", None)))))
|
| 354 |
+
if so_luong_thuc_te is None and isinstance(row, (pd.Series, dict)):
|
| 355 |
+
try:
|
| 356 |
+
so_luong_thuc_te = row.iloc[12]
|
| 357 |
+
except Exception:
|
| 358 |
+
continue
|
| 359 |
+
if pd.isna(so_luong_thuc_te):
|
| 360 |
+
continue
|
| 361 |
+
so_ngay_match = re.search(r"\*\s*(\d+(?:[.,]\d+|\/\d+)?)\s*ngày", lieu_dung)
|
| 362 |
+
so_luong_ngay_match = re.search(r"\[\s*(\d+(?:[.,]\d+|\/\d+)?)\s*[^\]]+\/ngày\]", lieu_dung)
|
| 363 |
+
if not so_ngay_match or not so_luong_ngay_match:
|
| 364 |
+
continue
|
| 365 |
+
try:
|
| 366 |
+
so_ngay = float(so_ngay_match.group(1).replace(",", "."))
|
| 367 |
+
so_luong_moi_ngay = float(so_luong_ngay_match.group(1).replace(",", "."))
|
| 368 |
+
so_luong_can_ke = so_ngay * so_luong_moi_ngay
|
| 369 |
+
try:
|
| 370 |
+
so_luong_thuc_te_val = float(str(so_luong_thuc_te).replace(",", "."))
|
| 371 |
+
except Exception:
|
| 372 |
+
continue
|
| 373 |
+
if not math.isclose(so_luong_can_ke, so_luong_thuc_te_val, rel_tol=1e-2):
|
| 374 |
+
wrong_qty_rows.append(idx)
|
| 375 |
+
except Exception:
|
| 376 |
+
continue
|
| 377 |
+
return wrong_qty_rows
|
| 378 |
+
|
| 379 |
+
def update_excel_with_errors(wb, insulin_errors, dose_errors, format_errors, unit_errors, wrong_qty_errors, output_path):
|
| 380 |
+
ws = wb.active
|
| 381 |
+
orange_fill = PatternFill(start_color="FFA500", end_color="FFA500", fill_type="solid") # Cam
|
| 382 |
+
yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
|
| 383 |
+
blue_fill = PatternFill(start_color="0088FF", end_color="0088FF", fill_type="solid")
|
| 384 |
+
red_fill = PatternFill(start_color="FF0000", end_color="FF0000", fill_type="solid")
|
| 385 |
+
purple_fill = PatternFill(start_color="845EC2", end_color="845EC2", fill_type="solid")
|
| 386 |
+
white_font = Font(color="FFFFFF")
|
| 387 |
+
insulin_set = set(insulin_errors)
|
| 388 |
+
wrong_qty_set = set(wrong_qty_errors)
|
| 389 |
+
unit_set = set(unit_errors)
|
| 390 |
+
dose_set = set(dose_errors)
|
| 391 |
+
format_set = set(format_errors)
|
| 392 |
+
for idx in range(2, ws.max_row + 1):
|
| 393 |
+
row_idx = idx - 2
|
| 394 |
+
if row_idx in insulin_set:
|
| 395 |
+
ws[f"I{idx}"].fill = orange_fill
|
| 396 |
+
elif row_idx in wrong_qty_set:
|
| 397 |
+
ws[f"I{idx}"].fill = yellow_fill
|
| 398 |
+
elif row_idx in unit_set:
|
| 399 |
+
ws[f"I{idx}"].fill = purple_fill
|
| 400 |
+
ws[f"I{idx}"].font = white_font
|
| 401 |
+
elif row_idx in format_set:
|
| 402 |
+
ws[f"I{idx}"].fill = red_fill
|
| 403 |
+
elif row_idx in dose_set:
|
| 404 |
+
ws[f"I{idx}"].fill = blue_fill
|
| 405 |
+
wb.save(output_path)
|
| 406 |
+
return output_path
|
| 407 |
+
|
| 408 |
+
def process_excel(file, insulin_file=None):
|
| 409 |
+
input_path = file.name
|
| 410 |
+
try:
|
| 411 |
+
df = pd.read_excel(input_path)
|
| 412 |
+
wb = openpyxl.load_workbook(input_path)
|
| 413 |
+
except Exception as e:
|
| 414 |
+
return None, None, None, None, None, None, f"❌ Lỗi khi đọc file Excel: {e}"
|
| 415 |
+
|
| 416 |
+
df = trim_column(df)
|
| 417 |
+
exceptions = load_exceptions()
|
| 418 |
+
allowed_exceptions = get_allowed_exceptions_tuples(DB_NL_FILE)
|
| 419 |
+
special_qty_tuples = get_special_qty_set_from_file(DB_TK_FILE)
|
| 420 |
+
special_format_tuples = get_special_format_set_from_file(DB_DINH_FILE)
|
| 421 |
+
special_unit_tuples = get_special_unit_set_from_file(DB_DVT_FILE)
|
| 422 |
+
lieu_exceptions = get_lieu_exceptions(DB_LIEU_FILE)
|
| 423 |
+
insulin_errors_cam, insulin_errors_xanh = highlight_insulin_errors(df, insulin_file) if insulin_file else ([], [])
|
| 424 |
+
|
| 425 |
+
dose_errors, format_errors = highlight_invalid_rows(df, exceptions, special_format_tuples, lieu_exceptions, insulin_errors_xanh, allowed_exceptions)
|
| 426 |
+
unit_errors = highlight_unit_errors(df, exceptions, special_unit_tuples, allowed_exceptions)
|
| 427 |
+
wrong_qty_errors = highlight_wrong_quantity_rows(df, special_qty_tuples, allowed_exceptions)
|
| 428 |
+
|
| 429 |
+
used = set(insulin_errors_cam)
|
| 430 |
+
wrong_qty_errors = [i for i in wrong_qty_errors if i not in used]
|
| 431 |
+
used |= set(wrong_qty_errors)
|
| 432 |
+
unit_errors = [i for i in unit_errors if i not in used]
|
| 433 |
+
used |= set(unit_errors)
|
| 434 |
+
format_errors = [i for i in format_errors if i not in used]
|
| 435 |
+
used |= set(format_errors)
|
| 436 |
+
dose_errors = [i for i in dose_errors if i not in used]
|
| 437 |
+
|
| 438 |
+
output_path = "ketqua_" + os.path.basename(input_path)
|
| 439 |
+
update_excel_with_errors(wb, insulin_errors_cam, dose_errors, format_errors, unit_errors, wrong_qty_errors, output_path)
|
| 440 |
+
|
| 441 |
+
format_path = "sai_dinhdang_" + os.path.basename(input_path)
|
| 442 |
+
dose_path = "sai_lieu_" + os.path.basename(input_path)
|
| 443 |
+
unit_path = "sai_donvitinh_" + os.path.basename(input_path)
|
| 444 |
+
qty_path = "sai_soluong_" + os.path.basename(input_path)
|
| 445 |
+
insulin_path = "insulin_soluong_" + os.path.basename(input_path)
|
| 446 |
+
df.iloc[format_errors].to_excel(format_path, index=False)
|
| 447 |
+
df.iloc[dose_errors].to_excel(dose_path, index=False)
|
| 448 |
+
df.iloc[unit_errors].to_excel(unit_path, index=False)
|
| 449 |
+
df.iloc[wrong_qty_errors].to_excel(qty_path, index=False)
|
| 450 |
+
if insulin_errors_cam:
|
| 451 |
+
df.iloc[insulin_errors_cam].to_excel(insulin_path, index=False)
|
| 452 |
+
else:
|
| 453 |
+
insulin_path = None
|
| 454 |
+
|
| 455 |
+
msg = []
|
| 456 |
+
if insulin_errors_cam:
|
| 457 |
+
msg.append(f"🟧 Có {len(insulin_errors_cam)} dòng Insulin sai số lượng chỉ định (ưu tiên màu cam).")
|
| 458 |
+
if insulin_errors_xanh:
|
| 459 |
+
msg.append(f"🔵 Có {len(insulin_errors_xanh)} dòng Insulin sai liều (tô xanh, không đúng tổng IU/lần/ngày).")
|
| 460 |
+
if wrong_qty_errors:
|
| 461 |
+
msg.append(f"🟡 Có {len(wrong_qty_errors)} dòng sai số lượng th��c kê.")
|
| 462 |
+
if dose_errors:
|
| 463 |
+
msg.append(f"🔵 Có {len(dose_errors)} dòng sai liều.")
|
| 464 |
+
if format_errors:
|
| 465 |
+
msg.append(f"🔴 Có {len(format_errors)} dòng sai định dạng.")
|
| 466 |
+
if unit_errors:
|
| 467 |
+
msg.append(f"🟣 Có {len(unit_errors)} dòng sai đơn vị tính.")
|
| 468 |
+
if not msg:
|
| 469 |
+
msg.append("✅ Không phát hiện lỗi nào.")
|
| 470 |
+
notice = "\n".join(msg)
|
| 471 |
+
return (
|
| 472 |
+
output_path,
|
| 473 |
+
format_path,
|
| 474 |
+
dose_path,
|
| 475 |
+
unit_path,
|
| 476 |
+
qty_path,
|
| 477 |
+
insulin_path,
|
| 478 |
+
notice
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
with gr.Blocks(title="Kiểm tra định dạng & liều dùng") as demo:
|
| 482 |
+
gr.Markdown("# 💊 Kiểm tra đơn thuốc ngoại trú")
|
| 483 |
+
gr.HTML("<span style='color:#0077b6;font-weight:bold'>Được tạo bởi DSCKI. Nguyễn Thành Trung - Bệnh viện Đa khoa khu vực Bảo Thắng.</span>")
|
| 484 |
+
with gr.Row():
|
| 485 |
+
with gr.Column():
|
| 486 |
+
file_input = gr.File(label="Tải lên file dinhdang 130")
|
| 487 |
+
insulin_input = gr.File(label="Tải lên file Insulin")
|
| 488 |
+
check_btn = gr.Button("✅ Kiểm tra",variant="primary")
|
| 489 |
+
with gr.Column():
|
| 490 |
+
notification = gr.Textbox(label="Thông báo", interactive=False,lines=4)
|
| 491 |
+
file_output = gr.File(label="File tổng kiểm tra (tô màu)")
|
| 492 |
+
file_format = gr.File(label="File lỗi định dạng")
|
| 493 |
+
file_dose = gr.File(label="File lỗi liều")
|
| 494 |
+
file_unit = gr.File(label="File lỗi đơn vị tính")
|
| 495 |
+
file_qty = gr.File(label="File lỗi số lượng thực kê")
|
| 496 |
+
file_insulin = gr.File(label="File lỗi Insulin (cam)")
|
| 497 |
+
|
| 498 |
+
check_btn.click(
|
| 499 |
+
fn=process_excel,
|
| 500 |
+
inputs=[file_input, insulin_input],
|
| 501 |
+
outputs=[file_output, file_format, file_dose, file_unit, file_qty, file_insulin, notification]
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
with gr.Tab("⚙️ Chỉnh sửa ngoại lệ"):
|
| 505 |
+
db_dinh_file = gr.File(label="DB_DINH")
|
| 506 |
+
db_dvt_file = gr.File(label="DB_DVT")
|
| 507 |
+
db_lieu_file = gr.File(label="DB_LIEU")
|
| 508 |
+
db_nl_file = gr.File(label="DB_NL")
|
| 509 |
+
db_tk_file = gr.File(label="DB_TK")
|
| 510 |
+
save_btn = gr.Button("💾 Cập nhật danh sách",variant="primary")
|
| 511 |
+
result_text = gr.Textbox(label="Trạng thái lưu", interactive=False)
|
| 512 |
+
save_btn.click(
|
| 513 |
+
fn=save_exceptions_from_files,
|
| 514 |
+
inputs=[db_tk_file, db_dinh_file, db_dvt_file, db_lieu_file, db_nl_file],
|
| 515 |
+
outputs=result_text
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
if __name__ == "__main__":
|
| 519 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
pandas
|
| 3 |
+
openpyxl
|