#!/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 helper.drug import milligram_to_gram, gram_to_milliliter from helper.report import query_logs, report_take_out_drug, report_put_in_detail_drug, report_take_out_detail_drug from helper.report 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 from helper.export import * 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 = [] spec: str | None cas_code: str | None custom_num: int | None rfid: str | None purity: str | None batch_no: str | None manufacturer: str | None position: str | None report_ids: list | None export_type: int | None = 0 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 spec: str | None dictionary_id: str | None cabinet_ids: list | None cas_code: str | None custom_num: int | None user_id: str | None state: int | None stime: str | None etime: str | None slack_day: int | None rfid: str | None purity: str | None batch_no: str | None manufacturer: str | None position: str | None report_ids: list | None page_no: int = 1 page_size: int = 10 @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.post('/inventory', summary="库存明细") async def inventory(request: Request, keyword: DrugSearch): """ 库存明细 根据大类展示库存明细 查询条件:药剂名称(目前暂定K1字段为药剂名称) -> 刷选后列出对应其它关键信息 查询条件如果有药剂名称,则查询药剂名称对应字典对应药剂,如果参数有字典id,直接查询对应字典对应药剂,如果两个都有,则联合查询 - 药剂信息(药剂名称、药剂规格、药剂纯度等组合)、CAS码、保质期、最后使用人、余量、状态(在库,出库)、位置,按药剂信息和保质期(最快到期在前)排序 - 查询成单独一类(以药剂字典为准)后,按余量少的排序在前 :return: """ data = await report_inventory_drug(request, keyword) return respond_to(code=200, data=data) @router.post('/put_in_detail', summary="入库明细") async def put_in_detail(request: Request, keyword: DrugSearch): data = await report_put_in_detail_drug(request, keyword) return respond_to(code=200, data=data) @router.post('/take_out_detail', summary="出库明细") async def take_out_detail(request: Request, keyword: DrugSearch): data = await report_take_out_detail_drug(request, keyword) return respond_to(code=200, data=data) @router.post('/use', summary="使用明细") async def use(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 remain(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('/take_out', summary="出库信息汇总") async def post(request: Request, keyword: DrugSearch): """ 出库信息汇总 - 提供时间区间查询,默认近365天。 - 药剂信息(药剂名称、药剂规格、药剂纯度等组合),出库量。 """ result = await report_take_out_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, # 库存明细 "put_in_detail": ReportPutInDetail, "take_out_detail": ReportTakeOutDetail, "use": ReportUse, # 使用明细 "slack": ReportSlack, # 呆滞物料 "put_in": ReportPutIn, # 入库统计 "take_out": ReportTakeOut, # 出库统计 "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' and not keyword.export_type: 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}'})