#!/usr/bin/env python # encoding: utf-8 """ @author: tx @file: drug.py @time: 2023/5/11 11:04 @desc: 药剂领用归还 """ import asyncio import datetime from decimal import Decimal from fastapi import APIRouter, Request, Depends from fastapi.encoders import jsonable_encoder from pydantic import BaseModel from tortoise.queryset import Q, QuerySet from tortoise.functions import Function from pypika import CustomFunction from conf import setting from helper.drug import drugs_except_info, drugs_list_info, gram_to_milligram, milligram_to_gram, open_update_expired_at from helper import respond_to, login_required from models import Dictionary, DrawerBoard, Drawer, User from models.drug import Drug, DrugStateEnum from models.terminal import Terminal from models.taboo import Taboo from models.cabinet import Cabinet from transfer.standard import Standard from transfer.code_standard import CodeStandard from helper.tool import parse_datetime from helper.log import logger_wrapper router = APIRouter(prefix='/drugs', dependencies=[Depends(login_required)]) return_router = APIRouter(prefix='/drugs/return',) class DrugUpdateRequest(BaseModel): open_date: str = "" # 开封日期 weight: int = 0 # 余量 use_weight: str | Decimal = 0.0 # 用量 rfid: str = "" id: str = "" cabinet_id: str = "" # 柜体id selectDrawer_id:str = "" class DrugUpdateExpired(BaseModel): expired_at: str rfid: str = "" id: str = "" class DrugRfids(BaseModel): rfids: list class DrugBackTrack(BaseModel): drug_list: list[dict] class DrugTakeOut(BaseModel): terminal_id: str | None page_no: int = 1 page_size: int = 20 class UpdateDictionary(BaseModel): lack_stock_count: int = 0 expiration_alert: int = 0 class EmptyBottleDisposal(BaseModel): rfid: str weight: int | None open_date: str | None class JsonExtract(Function): database_func=CustomFunction('JSON_EXTRACT', ['field', 'value']) class BlinkLocation(BaseModel): ip: str position: list[int] duration: int function: int # 查询药剂信息, @router.get('/get_drug_position_info',summary='获取试剂信息') async def get_drug_info(request:Request,rfid:str): if len(rfid)==0: return respond_to(404, desc="请传入药剂rfid") drug = await Drug.get_or_none(rfid=rfid) if not drug: return respond_to(404, desc="药剂信息不存在") drawer_obj = await Drawer.get_or_none(id=drug.drawer_id) if not drawer_obj: return respond_to(404, desc="药剂缺少drawer_id") drawer_number = drawer_obj.line_no drawer_board_obj = await DrawerBoard.filter(drawer_id =drawer_obj.id ).all() if not drawer_board_obj: return respond_to(404, desc="药剂绑定的层板drawer_board不存在") board_addresses = [] for item in drawer_board_obj: board_addresses.append(item.line_no) res={} res['drawer_number'] = drawer_number res['board_addresses'] = board_addresses return respond_to(200,desc="获取药剂位置信息成功",data=res) @router.post('/empty', summary='试剂置为空瓶') async def empty_bottle_disposal(request: Request, post: EmptyBottleDisposal): """ 试剂置为空瓶 :param request: Request请求头信息 :param post: 空瓶信息的请求体 :return: """ users = request.state.users standard = Standard(users) rfid = post.rfid drug = await Drug.get_or_none(rfid=rfid) if not drug: return respond_to(404, desc="药剂不存在") # user_weight = post.weight open_date = post.open_date if open_date: drug.open_date = parse_datetime(open_date, "%Y-%m-%d") await drug.save() await standard.empty(rfid) return respond_to() @router.post('/weight', summary="药剂用量或余量更新") async def update(request: Request, keyword: DrugUpdateRequest): """ 药剂用量或余量更新 接收id或rfid都可以更新重量 余量与用量二选一,只有一个有值 :param keyword: 包含更新信息的请求体 :return: """ print("open_date", keyword.open_date) rfid = keyword.rfid if keyword.rfid else "" if keyword.id: drug_obj = await Drug.get_or_none(id=keyword.id).prefetch_related("template") else: drug_obj = await Drug.get_or_none(Q(barcode=rfid) | Q(rfid=rfid)).prefetch_related("template") if not drug_obj: return respond_to(404, desc="药剂不存在") if keyword.weight: if not drug_obj.remain_gross_weight: drug_obj.remain_gross_weight = keyword.weight await drug_obj.save() else: # 余量转用量,更新用量 mill_weight = drug_obj.remain_gross_weight - keyword.weight await drug_obj.update_last_use_weight(weight=float(mill_weight)) elif keyword.use_weight: mill_weight = keyword.use_weight if keyword.use_weight and keyword.use_weight != "NaN" else 0 await drug_obj.update_last_use_weight(weight=float(mill_weight)) if keyword.open_date: if not drug_obj.open_date: drug_obj.open_date = parse_datetime(keyword.open_date, "%Y-%m-%d") template_obj = drug_obj.template new_expired_at = open_update_expired_at(drug_obj, template_obj, keyword.open_date) if new_expired_at: drug_obj.expired_at = new_expired_at await drug_obj.save() # 一维条码扫码归还更新重量后直接修改药剂状态,生成流转记录 rfid = drug_obj.rfid if len(rfid) < 16 and drug_obj.state == DrugStateEnum.OUT: cabinet_id = keyword.cabinet_id if keyword.cabinet_id else request.state.client_id if not cabinet_id: cabinet_obj = await Cabinet.filter(terminal_id=setting.TERMINAL_ID).first() else: cabinet_obj = await Cabinet.get_or_none(id=cabinet_id) if cabinet_obj: # 位置更新 position_str = f"{cabinet_obj.label}" drug_obj.position = position_str drug_obj.last_return_at = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") open_date = drug_obj.open_date if not open_date: drug_obj.open_date = datetime.datetime.now() drug_obj.last_return_id = request.state.current_user.id drug_obj.cabinet_id = cabinet_obj.id if keyword.selectDrawer_id: drawer_board = await DrawerBoard.filter(id = keyword.selectDrawer_id).get_or_none() if drawer_board: drug_obj.drawer_id = drawer_board.drawer_id drug_obj.drawer_board_id = keyword.selectDrawer_id await drug_obj.save() # 一维条码生成流转记录 standard = CodeStandard(request.state.users, drug_obj) await standard.put_in(barcode=drug_obj.barcode if len(drug_obj.rfid) < 16 else drug_obj.rfid) return respond_to(code=200, desc="更新成功") return respond_to(code=200, desc="更新成功") @router.post('/except', summary='RFID药剂异常信息') async def index(request: DrugRfids): """ 获取RFID药剂异常信息 :return: """ if not request.rfids: return respond_to(code=200, desc="", data=[]) result = list() drugs = await Drug.filter(rfid__in=request.rfids).prefetch_related('dictionary', 'template').all() # terminal_obj = await Terminal.get(id=setting.TERMINAL_ID) taboo_list = [] # 130个rfid,如何快速获取里面的异常标签信息 # 并发执行多个任务 tasks = [drugs_except_info(drug, "") for drug in drugs] results = await asyncio.gather(*tasks) for drug, data in zip(drugs, results): data["rfid"] = drug.rfid result.append(data) # 药剂禁忌判断 if not drug.taboo_species: continue else: taboo_list_str = ",".join(set(taboo_list)) if taboo_list else "" trigger, taboo_desc = await Taboo.conflict(drug.taboo_species, taboo_list_str) if trigger: result.append({ "rfid": "", "is_rfid_except": True, "sound": True, "message": taboo_desc, "state": 6, }) taboo_list.append(drug.taboo_species) # for drug in drugs: # data = await drugs_except_info(drug, "") # data["rfid"] = drug.rfid # result.append(data) # # 药剂禁忌判断 # if not drug.taboo_species: # continue # else: # taboo_list_str = ",".join(set(taboo_list)) if taboo_list else "" # trigger, taboo_desc = await Taboo.conflict(drug.taboo_species, taboo_list_str) # if trigger: # result.append({ # "rfid": "", # "is_rfid_except": True, # "sound": True, # "message": taboo_desc, # "state": 6, # }) # taboo_list.append(drug.taboo_species) # 传入rfid与data所有rfid比较,差集就是非法标签 post_rfid = set(request.rfids) effective_rfid = set([i["rfid"] for i in result]) diff_rfid = post_rfid - effective_rfid if diff_rfid: result.extend([{ "rfid": rfid, "is_rfid_except": True, "sound": True, "message": "非法标签", "state": 0, } for rfid in diff_rfid]) return respond_to(code=200, desc="获取RFID药剂信息成功", data=result) @router.post('', summary='RFID药剂详情') async def index(request: DrugRfids): """ 获取RFID药剂信息 药剂信息详情 :return: """ drugs = list() for rfid in request.rfids: drug_obj = await Drug.get_or_none(rfid=rfid).prefetch_related('dictionary', 'template', 'drawer_board', 'template__archive') if drug_obj is None: drugs.append({ "drugs_except_info": { "sound": False, "message": "非法标签", "state": 0, }, "rfid_drug": {rfid: f"[{rfid}]未知药剂"} }) continue result = jsonable_encoder(drug_obj) template_obj = drug_obj.template terminal_obj = await Terminal.get(id=setting.TERMINAL_ID) data = await drugs_except_info(drug_obj, terminal_obj) drug_info = await drug_obj.attribute_drug_info() parse_fill_json_content = await drug_obj.parse_fill_json_content() parse_fill_json_content.update({f'余重': f'{milligram_to_gram(drug_obj.remain_gross_weight)}'}) return_require_weigh = template_obj.archive.params.get("return_require_weigh", '') drug_info_dict = { "drugs_except_info": data, "fill_json_content": parse_fill_json_content, "rfid_drug": {drug_obj.rfid: ",".join(list(map(lambda x:str(x), drug_info.values())))}, "return_require_weigh": return_require_weigh } # 2000型号柜子,需要灯光指引,返回值添加灯光序号参数 if setting.CLIENT_NUMBER == "2000": if drug_obj.drawer_board: drawer_board_line_no = drug_obj.drawer_board.line_no # 亮灯位置 = 层数*36+编号 blink_no = (drawer_board_line_no - 1) * 36 + drug_obj.hole drug_info_dict.update({"blink_no": blink_no}) result.update(drug_info_dict) drugs.append(result) return respond_to(code=200, desc="获取RFID药剂信息成功", data=drugs) @router.get('/take_out', summary='药剂领用查询') async def take_out_index(request: Request, drug_name: str = '', take_out_type: int = 0, page_no: int = 1, page_size: int = 20): """ 药剂领用列表查询 本终端的排前面 默认按照入库时间倒叙,条件查询按照有效期倒叙 take_out_type 本终端0 全局终端1 :return: """ terminal = await Terminal.get(id=setting.TERMINAL_ID) current_user = request.state.current_user order_key = "-created_at" # 普通用户只能领用自己的药剂,管理员可以领取所有药剂 role = await current_user.role if role.grade >= 50: query = Drug.filter(state=DrugStateEnum.IN) else: query = Drug.filter(state=DrugStateEnum.IN, rfid__in=current_user.keys) # 本终端的时候查看当前登录柜子的药剂, 全局终端查看大类所有柜子药剂 if not take_out_type: query = query.filter(cabinet__terminal=terminal) # 如果带辅柜则只查询选择的柜体药剂 if request.state and request.state.client_id: query = query.filter(cabinet_id=request.state.client_id) else: # 全局终端 大类对应所有柜子 cabinet_ids = await Cabinet.filter(archive_id=request.state.archive_id).values_list('id', flat=True) query = query.filter(cabinet_id__in=cabinet_ids) if drug_name: annotate = {} annotate['json_batch_no'] = JsonExtract('fill_json_content', '$.cas_number') query = query.annotate(**annotate).filter( Q(json_batch_no__contains=drug_name) | Q(fill_json_content__filter={"seq_no": drug_name}) | Q( dictionary__k1__contains=drug_name) | Q(dictionary__k2__contains=drug_name)) order_key = "-expired_at" total_count = await query.count() offset = (page_no - 1) * page_size drugs = await query.offset(offset).order_by(order_key).limit(page_size).all() result = await drugs_list_info(drugs) return respond_to(200, data=dict(count=total_count, drugs=result)) @router.get('/put_in', summary='获取当前用户待归还清单') async def put_in_index(request: Request): """ 获取当前用户待归还清单 :return: """ current_user = request.state.current_user cabinet_ids = await Cabinet.filter(terminal_id=setting.TERMINAL_ID).values_list('id', flat=True) # 当前柜体待归还清单 query = Drug.filter(last_receive_id=current_user.id, state=2).filter(cabinet_id__in=cabinet_ids) drugs = await query.all() result = await drugs_list_info(drugs) return respond_to(data=result) @router.get('/keys_list', summary='获取钥匙清单') async def get_keys_list(request: Request): """ 获取钥匙清单 :return: """ current_user = request.state.current_user role = await current_user.role if role.grade >= 50: query = Drug.filter(state__in=[DrugStateEnum.IN, DrugStateEnum.OUT]) drugs = await query.all() result = await drugs_list_info(drugs) return respond_to(data=result) else: return respond_to(code=403, desc="没有权限") @router.put('/expired_at', summary='药剂信息修改') @logger_wrapper async def update(keyword: DrugUpdateExpired): """ 药剂信息过期时间修改 :param id: 用户药剂模板条目id :param keyword: :return: """ rfid = keyword.rfid if keyword.rfid else "" query_param = {"id": keyword.id} if keyword.id else {"rfid": rfid} drug_obj = await Drug.get(**query_param) drug_obj.expired_at = keyword.expired_at await drug_obj.save() return respond_to(code=200, desc="更新成功") @router.get('/dictionaries', summary='获取药剂字典列表') async def index(request: Request, keyword: str = '', page_no: int = 1, page_size: int = 10): """ 获取本终端药剂字典列表 :param keyword: 按药剂信息搜索 :param page_no: 分页页码,默认为1 :param page_size: 分页大小,默认为10 :return: """ query = QuerySet(Dictionary).filter() archive_id = request.state.archive_id if archive_id: query = query.filter(archive_id=archive_id) if keyword: keyword = keyword.strip() query = query.filter(Q(k1__icontains=keyword) | Q(k2__icontains=keyword) | Q(k3__icontains=keyword) | Q(k4__icontains=keyword) | Q(k5__icontains=keyword) | Q(k6__icontains=keyword)) offset = (page_no - 1) * page_size count = await query.count() dictionary_objs = await query.limit(page_size).offset(offset).order_by('-created_at').values() result_list = [] for obj in dictionary_objs: drug_info = ",".join([value for key, value in obj.items() if key.startswith('k') and value is not None]) item = dict(obj) if obj['params']: for k, v in obj['params'].items(): item[k] = v item.pop("params") item['drug_info'] = drug_info result_list.append(item) return respond_to(data={'data': result_list, 'count': count}) @router.put('/dictionary/{id}', summary='编辑药剂字典') async def update(id: str, post: UpdateDictionary): """ 编辑药剂字典 :param id: id :param post: :return: """ dictionary = await Dictionary.get(id=id) dictionary.params = post.dict() await dictionary.save() return respond_to() @router.post('/backtrack', summary='药剂是否原路返回') async def index(request: Request, keyword: DrugBackTrack): """ 判断药剂是否原路返回 drug_list[{ rfid: str cabinet_id: str drawer_id: str hole: int = 0 }] :return: """ if setting.BACKTRACK == 0: return respond_to(200, data=[], desc="药剂不需要原路返回") drugs = list() for drug in keyword.drug_list: drug_obj = await Drug.get_or_none(rfid=drug.get("rfid")).prefetch_related('template__archive') if drug_obj is None: continue if drug_obj.cabinet_id != drug.get("cabinet_id") or \ drug_obj.drawer_id != drug.get("drawer_id") or \ drug.get("hole") and drug_obj.hole != drug.get("hole"): drug_info = await drug_obj.attribute_drug_info() drug_info_comment = ",".join(list(map(lambda x: str(x), drug_info.values()))) drugs.append({ "drugs_except_info": { "sound": False, "message": f"[{drug_info_comment}]药剂放入位置不正确", "state": 5, }, }) return respond_to(200, data=drugs) @router.post('/blink', summary='灯光闪烁接口') async def blink(request: BlinkLocation): import httpx post_data={} post_data["position"] = request.position post_data["duration"] = request.duration post_data["function"] = request.function url = "http://"+request.ip+"/api/cabinet/v1/led" async with httpx.AsyncClient() as client: try: response = await client.post(url, json=post_data) except Exception: return respond_to(code=400, desc='下位机通讯失败') return respond_to() @return_router.post("/info", summary='RFID药剂详情(免登录)') async def drug_info(request: DrugRfids): """ 获取RFID药剂信息 药剂信息详情 :return: """ drugs = list() for rfid in request.rfids: drug_obj = await Drug.get_or_none(rfid=rfid).prefetch_related('dictionary', 'template', 'drawer_board', 'template__archive') if drug_obj is None: drugs.append({ "drugs_except_info": { "sound": False, "message": "非法标签", "state": 0, }, "rfid_drug": {rfid: f"[{rfid}]未知药剂"} }) continue result = jsonable_encoder(drug_obj) template_obj = drug_obj.template terminal_obj = await Terminal.get(id=setting.TERMINAL_ID) data = await drugs_except_info(drug_obj, terminal_obj) drug_info = await drug_obj.attribute_drug_info() parse_fill_json_content = await drug_obj.parse_fill_json_content() parse_fill_json_content.update({f'余重': f'{milligram_to_gram(drug_obj.remain_gross_weight)}'}) return_require_weigh = template_obj.archive.params.get("return_require_weigh", '') drug_info_dict = { "drugs_except_info": data, "fill_json_content": parse_fill_json_content, "rfid_drug": {drug_obj.rfid: ",".join(list(map(lambda x:str(x), drug_info.values())))}, "return_require_weigh": return_require_weigh } # 2000型号柜子,需要灯光指引,返回值添加灯光序号参数 if setting.CLIENT_NUMBER == "2000": if drug_obj.drawer_board: drawer_board_line_no = drug_obj.drawer_board.line_no # 亮灯位置 = 层数*36+编号 blink_no = (drawer_board_line_no - 1) * 36 + drug_obj.hole drug_info_dict.update({"blink_no": blink_no}) result.update(drug_info_dict) drugs.append(result) return respond_to(code=200, desc="获取RFID药剂信息成功", data=drugs) @return_router.post('/weight', summary="药剂用量或余量更新(快速归还)") async def return_drug(request: Request, keyword: DrugUpdateRequest): """ 药剂用量或余量更新 接收id或rfid都可以更新重量 余量与用量二选一,只有一个有值 :param keyword: 包含更新信息的请求体 :return: """ print("open_date", keyword.open_date) rfid = keyword.rfid if keyword.rfid else "" if keyword.id: drug_obj = await Drug.filter(id=keyword.id).prefetch_related("template","last_receive").first() else: drug_obj = await Drug.filter(Q(barcode=rfid) | Q(rfid=rfid)).prefetch_related("template","last_receive").first() if not drug_obj: return respond_to(404, desc="药剂不存在") syr = drug_obj.fill_json_content.get('syr') # 入库模板药剂使用人 syr_user = await User.get_or_none(name=syr.replace(' ', '')) # 用户默认为药剂使用人 current_user = syr_user if syr_user else drug_obj.last_receive # 柜子的当前使用人更新,盘点时需要 cabinet_id = keyword.cabinet_id if not cabinet_id: cabinet_obj = await Cabinet.filter(terminal_id=setting.TERMINAL_ID).first() else: cabinet_obj = await Cabinet.get_or_none(id=cabinet_id) cabinet_obj.user_id = current_user.id await cabinet_obj.save() if keyword.weight: if not drug_obj.remain_gross_weight: drug_obj.remain_gross_weight = keyword.weight await drug_obj.save() else: # 余量转用量,更新用量 mill_weight = drug_obj.remain_gross_weight - keyword.weight await drug_obj.update_last_use_weight(weight=float(mill_weight)) elif keyword.use_weight: mill_weight = keyword.use_weight if keyword.use_weight and keyword.use_weight != "NaN" else 0 await drug_obj.update_last_use_weight(weight=float(mill_weight)) if keyword.open_date: if not drug_obj.open_date: drug_obj.open_date = parse_datetime(keyword.open_date, "%Y-%m-%d") template_obj = drug_obj.template new_expired_at = open_update_expired_at(drug_obj, template_obj, keyword.open_date) if new_expired_at: drug_obj.expired_at = new_expired_at await drug_obj.save() # 一维条码扫码归还更新重量后直接修改药剂状态,生成流转记录 rfid = drug_obj.rfid if len(rfid) < 16 and drug_obj.state == DrugStateEnum.OUT: if cabinet_obj: # 位置更新 position_str = f"{cabinet_obj.label}" drug_obj.position = position_str drug_obj.last_return_at = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") open_date = drug_obj.open_date if not open_date: drug_obj.open_date = datetime.datetime.now() # drug_obj.last_return_id = request.state.current_user.id drug_obj.last_return_id = current_user.id drug_obj.cabinet_id = cabinet_obj.id if keyword.selectDrawer_id: drawer_board = await DrawerBoard.filter(id = keyword.selectDrawer_id).get_or_none() if drawer_board: drug_obj.drawer_id = drawer_board.drawer_id drug_obj.drawer_board_id = keyword.selectDrawer_id await drug_obj.save() # 一维条码生成流转记录 standard = CodeStandard([current_user.id,], drug_obj) await standard.put_in(barcode=drug_obj.barcode if len(drug_obj.rfid) < 16 else drug_obj.rfid) return respond_to(code=200, desc="更新成功") return respond_to(code=200, desc="更新成功") @return_router.post('/empty', summary='试剂置为空瓶') async def return_empty(request: Request, post: EmptyBottleDisposal): """ 试剂置为空瓶 :param request: Request请求头信息 :param post: 空瓶信息的请求体 :return: """ # users = request.state.users # standard = Standard(users) rfid = post.rfid drug = await Drug.filter(rfid=rfid).prefetch_related("last_receive").first() if not drug: return respond_to(404, desc="药剂不存在") # user_weight = post.weight open_date = post.open_date if open_date: drug.open_date = parse_datetime(open_date, "%Y-%m-%d") await drug.save() syr = drug.fill_json_content.get('syr') # 入库模板药剂使用人 syr_user = await User.get_or_none(name=syr.replace(' ', '')) # 用户默认为药剂使用人 current_user = syr_user if syr_user else drug.last_receive standard = Standard([current_user.id,]) await standard.empty(rfid) return respond_to()