|
|
#!/usr/bin/env python
|
|
|
# encoding: utf-8
|
|
|
"""
|
|
|
@author: tx
|
|
|
@file: report.py
|
|
|
@time: 2023/6/9 15:33
|
|
|
@desc:
|
|
|
"""
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
import pytz
|
|
|
from fastapi import APIRouter, Request, Depends, Response
|
|
|
from fastapi.encoders import jsonable_encoder
|
|
|
from pydantic import BaseModel
|
|
|
from tortoise.functions import Count, Sum
|
|
|
from tortoise.queryset import Q
|
|
|
from tortoise.queryset import QuerySet
|
|
|
|
|
|
from conf import setting
|
|
|
from .export import *
|
|
|
from .report_cab import report_inventory_drug, report_use_drug, report_slack_drug, report_put_in_drug, \
|
|
|
report_remain_drug, report_dosage_drug, report_cabinet_environmental
|
|
|
from helper import respond_to, login_required
|
|
|
from helper.tool import parse_datetime
|
|
|
from models import User
|
|
|
from models.dictionary import Dictionary
|
|
|
from models.drug import Drug, DrugStateEnum
|
|
|
from models.drug_use_log import DrugUseLog, DrugUseStateEnum
|
|
|
from models.log import EnvironmentLogs
|
|
|
from models.template import Template
|
|
|
from models.cabinet import Cabinet
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix='', dependencies=[Depends(login_required)])
|
|
|
|
|
|
|
|
|
class SearchWord(BaseModel):
|
|
|
slack_day: int = 0
|
|
|
drug_name: str | None
|
|
|
dictionary_id: str | None
|
|
|
user_id: str | None
|
|
|
state: int | None
|
|
|
stime: str | None
|
|
|
etime: str | None
|
|
|
slack_day: int | None
|
|
|
page_no: int = 1
|
|
|
page_size: int = 10
|
|
|
cabinet_ids: list = []
|
|
|
cabinet_id: str | None
|
|
|
|
|
|
|
|
|
async def parse_cabinet_ids():
|
|
|
cabinet_obj = await Cabinet.filter(terminal_id=setting.TERMINAL_ID).prefetch_related("archive").first()
|
|
|
cabinets_ids = await Cabinet.filter(archive_id=cabinet_obj.archive.id).values_list("id", flat=True)
|
|
|
return cabinets_ids
|
|
|
|
|
|
|
|
|
async def parse_archive_cabinet_ids(archive_id: str, client_ids: list):
|
|
|
"""
|
|
|
根据大类ID获取柜体
|
|
|
如果传递客户端ids参数,则返回用户有权限的大类柜体列表
|
|
|
:param archive_id:
|
|
|
:param client_ids: 用户有权限的柜体id,如果不传则代表全部柜体
|
|
|
:return:
|
|
|
"""
|
|
|
query = QuerySet(Cabinet).filter()
|
|
|
if archive_id:
|
|
|
query = query.filter(archive_id=archive_id)
|
|
|
if client_ids:
|
|
|
query = query.filter(id__in=client_ids)
|
|
|
# 用户有权限的柜体
|
|
|
cabinets_ids = await query.values_list("id", flat=True)
|
|
|
return cabinets_ids
|
|
|
|
|
|
|
|
|
class DrugSearch(BaseModel):
|
|
|
drug_name: str | None
|
|
|
dictionary_id: str | None
|
|
|
cabinet_ids: list | None
|
|
|
user_id: str | None
|
|
|
state: int | None
|
|
|
stime: str | None
|
|
|
etime: str | None
|
|
|
slack_day: int | None
|
|
|
cabinet_id: str | None
|
|
|
page_no: int = 1
|
|
|
page_size: int = 10
|
|
|
rfid:str | None
|
|
|
spec:str | None
|
|
|
purity:str | None
|
|
|
batch_no:str | None
|
|
|
manufacturer:str | None
|
|
|
position:str | None
|
|
|
report_ids:str | None
|
|
|
|
|
|
@router.get('/archives/{drug_name}', summary='大类下拉選項')
|
|
|
async def index(drug_name: str):
|
|
|
"""
|
|
|
大类下拉選項
|
|
|
drug_name: 试剂名称
|
|
|
:return:
|
|
|
"""
|
|
|
result_list = []
|
|
|
dictionary = await Dictionary.all()
|
|
|
for dic_obj in dictionary:
|
|
|
if drug_name in dic_obj.k1:
|
|
|
keys = ['k1', 'k2', 'k3', 'k4', 'k5', 'k6']
|
|
|
result = ','.join([str(getattr(dic_obj, key)) for key in keys if getattr(dic_obj, key)])
|
|
|
result_list.append({'drug_info': result, 'id': dic_obj.id})
|
|
|
return respond_to(data=result_list)
|
|
|
|
|
|
|
|
|
# 库存明细
|
|
|
@router.get('/inventory', summary="库存明细")
|
|
|
async def index(request: Request, drug_type: str = '', drug_name: str = '', dictionary_id: str = '', page_no: int = 1,
|
|
|
page_size: int = 20, state: int = -1, cabinet_id: str = ''):
|
|
|
"""
|
|
|
库存明细
|
|
|
根据大类展示库存明细
|
|
|
库存明细增加状态和位置筛选
|
|
|
|
|
|
查询条件:药剂名称(目前暂定K1字段为药剂名称) -> 刷选后列出对应其它关键信息
|
|
|
查询条件如果有药剂名称,则查询药剂名称对应字典对应药剂,如果参数有字典id,直接查询对应字典对应药剂,如果两个都有,则联合查询
|
|
|
- 药剂信息(药剂名称、药剂规格、药剂纯度等组合)、CAS码、保质期、最后使用人、余量、状态(在库,出库)、位置,按药剂信息和保质期(最快到期在前)排序
|
|
|
- 查询成单独一类(以药剂字典为准)后,按余量少的排序在前
|
|
|
:return:
|
|
|
"""
|
|
|
keyword = {
|
|
|
"drug_name": drug_name,
|
|
|
"dictionary_id": dictionary_id,
|
|
|
"page_no": page_no,
|
|
|
"page_size": page_size,
|
|
|
"state": state,
|
|
|
"cabinet_id": cabinet_id
|
|
|
}
|
|
|
if drug_type == "box":
|
|
|
# 盒装药剂库存明细
|
|
|
# data = await box_report_inventory_drug(request, keyword)
|
|
|
data = await report_inventory_drug(request, keyword)
|
|
|
else:
|
|
|
data = await report_inventory_drug(request, keyword)
|
|
|
return respond_to(code=200, data=data)
|
|
|
|
|
|
|
|
|
@router.post('/use', summary="使用明细")
|
|
|
async def index(request: Request, keyword: DrugSearch):
|
|
|
"""
|
|
|
使用明细
|
|
|
- 管理员可通过【下拉用户列表】查询,普通用户无查询功能
|
|
|
- 查询条件:药剂名称(目前暂定K1字段为药剂名称) -> 刷选后列出对应其它关键信息
|
|
|
- 药剂信息(药剂名称、药剂规格、药剂纯度等组合)、CAS码按领用时间倒序
|
|
|
逻辑:查询所有未归还药剂,所有其他药剂
|
|
|
- 未归还药剂使用记录,按时间倒叙
|
|
|
- 其他药剂使用记录,按时间倒叙
|
|
|
- 列表合并之后获取总数与分页
|
|
|
:return:
|
|
|
"""
|
|
|
data = await report_use_drug(request, keyword)
|
|
|
return respond_to(data=data)
|
|
|
|
|
|
|
|
|
@router.post('/remain', summary="库存量统计")
|
|
|
async def index(request: Request, keyword: DrugSearch):
|
|
|
"""
|
|
|
库存量统计
|
|
|
# - 查询条件:药剂名称(目前暂定K1字段为药剂名称) -> 刷选后列出对应其它关键信息
|
|
|
# - 药剂信息(药剂名称、药剂规格、药剂纯度等组合,以药剂字典为准)、瓶数,查看明细进入库存明细表
|
|
|
"""
|
|
|
result = await report_remain_drug(request, keyword)
|
|
|
|
|
|
return respond_to(data=result)
|
|
|
|
|
|
|
|
|
@router.post('/put_in', summary="入库统计")
|
|
|
async def post(request: Request, keyword: DrugSearch):
|
|
|
"""
|
|
|
入库统计
|
|
|
- 提供时间区间查询,默认近365天。
|
|
|
- 药剂信息(药剂名称、药剂规格、药剂纯度等组合),入库量。
|
|
|
"""
|
|
|
result = await report_put_in_drug(request, keyword)
|
|
|
|
|
|
return respond_to(200, data=result)
|
|
|
|
|
|
|
|
|
@router.post('/dosage', summary="药剂用量")
|
|
|
async def index(request: Request, keyword: DrugSearch):
|
|
|
"""
|
|
|
药剂用量
|
|
|
- 提供时间区间查询,默认近30天。
|
|
|
- 用户每类(药剂字典)领用药剂的使用总量
|
|
|
"""
|
|
|
result = await report_dosage_drug(request, keyword)
|
|
|
return respond_to(200, desc=result.get("desc"), data=result)
|
|
|
|
|
|
|
|
|
@router.get('/slack', summary="呆滞物料")
|
|
|
async def index(request: Request, drug_name: str = "", slack_day: int = 30, page_no: int = 1, page_size: int = 20):
|
|
|
"""
|
|
|
呆滞物料
|
|
|
- 查询多少天内未领用的药剂,默认30天,可自定义填写。
|
|
|
- 显示每一瓶药剂信息
|
|
|
"""
|
|
|
keyword = {
|
|
|
"drug_name": drug_name,
|
|
|
"slack_day": slack_day,
|
|
|
"page_no": page_no,
|
|
|
"page_size": page_size
|
|
|
}
|
|
|
result = await report_slack_drug(request, keyword)
|
|
|
return respond_to(200, data=result)
|
|
|
|
|
|
|
|
|
@router.get('/environmental', summary="环境记录")
|
|
|
async def index(request: Request, cabinet_id: str = "", stime: str = "", etime: str = "", page_no: int = 1, page_size:
|
|
|
int = 10):
|
|
|
"""
|
|
|
环境记录报表
|
|
|
- 温度、湿度、voc信息由服务端定期发起采集
|
|
|
- 记录下位机日志
|
|
|
- 提供时间区间查询,默认近30天。
|
|
|
"""
|
|
|
offset = (page_no - 1) * page_size
|
|
|
query = QuerySet(EnvironmentLogs).filter(cabinet__terminal=setting.TERMINAL_ID)
|
|
|
# 都为空则不显示
|
|
|
query = query.filter(
|
|
|
Q(left_temperature__isnull=False) | Q(right_temperature__isnull=False) | Q(voc__isnull=False) | Q(
|
|
|
humidity__isnull=False))
|
|
|
if cabinet_id:
|
|
|
query = query.filter(cabinet_id=cabinet_id)
|
|
|
if stime:
|
|
|
query = query.filter(created_at__gte=f"{stime} 00:00:00")
|
|
|
if etime:
|
|
|
query = query.filter(created_at__lte=f"{etime} 23:59:59")
|
|
|
if not stime and not etime:
|
|
|
stime = datetime.now() - timedelta(days=30)
|
|
|
etime = datetime.now()
|
|
|
query = query.filter(created_at__gte=stime, created_at__lte=etime)
|
|
|
count = await query.count()
|
|
|
en_log_objs = await query.prefetch_related("cabinet").limit(page_size).offset(offset).order_by("-created_at")
|
|
|
result = list()
|
|
|
for en_log_obj in en_log_objs:
|
|
|
result.append({
|
|
|
**jsonable_encoder(en_log_obj),
|
|
|
"cabinet_label": en_log_obj.cabinet.label
|
|
|
})
|
|
|
return respond_to(200, data=dict(count=count, data=result))
|
|
|
|
|
|
|
|
|
@router.post('/export/{key}', summary="报表导出")
|
|
|
async def index(request: Request, key: str, keyword: SearchWord):
|
|
|
export_dict = {
|
|
|
"drug_logs": ReportDrugUseLogs, # 流转日志
|
|
|
"inventory": ReportInventory, # 库存明细
|
|
|
"use": ReportUse, # 使用明细
|
|
|
"slack": ReportSlack, # 呆滞物料
|
|
|
"put_in": ReportPutIn, # 入库统计
|
|
|
"put_in1": ReportPutIn1,#入库验证f
|
|
|
"remain": ReportRemain, # 库存量统计
|
|
|
"dosage": ReportDosage, # 试剂用量
|
|
|
"reagentExpiration": ReportReagentExpiration, # 试剂过期
|
|
|
"environmental": ReportEnvironmental, # 环境记录报表
|
|
|
}
|
|
|
if not export_dict.get(key):
|
|
|
return respond_to(404, "请求参数错误,该报表导出暂未实现")
|
|
|
|
|
|
export_obj = export_dict.get(key)(request, keyword)
|
|
|
await export_obj.main()
|
|
|
kwargs = {
|
|
|
'data_list': export_obj.data_list,
|
|
|
'key_list': export_obj.key_list,
|
|
|
'finds_list': export_obj.finds_list,
|
|
|
}
|
|
|
export_obj.build_file(title=export_obj.title,**kwargs)
|
|
|
if key !='put_in1':
|
|
|
binary, encoded_filename = export_obj.export('usb')
|
|
|
else:
|
|
|
binary, encoded_filename = export_obj.export('',download_type='web')
|
|
|
if isinstance(binary, int):
|
|
|
return respond_to(binary, desc=encoded_filename)
|
|
|
return Response(content=binary.getvalue(),
|
|
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
|
headers={'Content-Disposition': f'attachment; filename={encoded_filename}'})
|