|
|
@@ -9,6 +9,8 @@ from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
|
|
|
from app.core.response import success_response, error_response
|
|
|
from app.crud import consumable as consumable_svc
|
|
|
from app.crud import reagent as reagent_svc
|
|
|
+from app.crud import consumable_stock_log as consumable_stock_log_svc
|
|
|
+from app.crud import reagent_stock_log as reagent_stock_log_svc
|
|
|
from app.crud import sop_document as sop_document_svc
|
|
|
from app.crud import process_category as process_category_svc
|
|
|
from app.crud import output_type as output_type_svc
|
|
|
@@ -178,6 +180,171 @@ async def delete_consumable(
|
|
|
return error_response(400, str(e))
|
|
|
|
|
|
|
|
|
+class StockInBody(BaseModel):
|
|
|
+ model_config = ConfigDict(populate_by_name=True)
|
|
|
+ qty: float = Field(..., gt=0)
|
|
|
+ remark: Optional[str] = None
|
|
|
+
|
|
|
+
|
|
|
+class StockOutBody(BaseModel):
|
|
|
+ model_config = ConfigDict(populate_by_name=True)
|
|
|
+ qty: float = Field(..., gt=0)
|
|
|
+ receiver: str = Field(..., min_length=1)
|
|
|
+ purpose: Optional[str] = None
|
|
|
+ remark: Optional[str] = None
|
|
|
+
|
|
|
+
|
|
|
+class BatchStockInItem(BaseModel):
|
|
|
+ model_config = ConfigDict(populate_by_name=True)
|
|
|
+ materialId: str
|
|
|
+ qty: float = Field(..., gt=0)
|
|
|
+ remark: Optional[str] = None
|
|
|
+
|
|
|
+
|
|
|
+class BatchStockInBody(BaseModel):
|
|
|
+ model_config = ConfigDict(populate_by_name=True)
|
|
|
+ items: List[BatchStockInItem]
|
|
|
+ remark: Optional[str] = None
|
|
|
+
|
|
|
+
|
|
|
+class BatchStockOutItem(BaseModel):
|
|
|
+ model_config = ConfigDict(populate_by_name=True)
|
|
|
+ materialId: str
|
|
|
+ qty: float = Field(..., gt=0)
|
|
|
+
|
|
|
+
|
|
|
+class BatchStockOutBody(BaseModel):
|
|
|
+ model_config = ConfigDict(populate_by_name=True)
|
|
|
+ items: List[BatchStockOutItem]
|
|
|
+ receiver: str = Field(..., min_length=1)
|
|
|
+ purpose: Optional[str] = None
|
|
|
+ remark: Optional[str] = None
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/consumables/{consumable_id}/stock-in")
|
|
|
+async def consumable_stock_in(
|
|
|
+ consumable_id: str,
|
|
|
+ body: StockInBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ from decimal import Decimal
|
|
|
+ obj = consumable_svc.stock_in(
|
|
|
+ db, consumable_id,
|
|
|
+ qty=Decimal(str(body.qty)),
|
|
|
+ remark=body.remark,
|
|
|
+ user_id=user.get("user_id"),
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ )
|
|
|
+ return success_response(consumable_svc._row_to_dict(obj), message="入库成功")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/consumables/{consumable_id}/stock-out")
|
|
|
+async def consumable_stock_out(
|
|
|
+ consumable_id: str,
|
|
|
+ body: StockOutBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ from decimal import Decimal
|
|
|
+ obj = consumable_svc.stock_out(
|
|
|
+ db, consumable_id,
|
|
|
+ qty=Decimal(str(body.qty)),
|
|
|
+ receiver=body.receiver,
|
|
|
+ purpose=body.purpose,
|
|
|
+ remark=body.remark,
|
|
|
+ user_id=user.get("user_id"),
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ )
|
|
|
+ return success_response(consumable_svc._row_to_dict(obj), message="出库成功")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/consumables/batch-stock-in")
|
|
|
+async def consumable_batch_stock_in(
|
|
|
+ body: BatchStockInBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items_data = [it.model_dump() for it in body.items]
|
|
|
+ count = consumable_svc.batch_stock_in(
|
|
|
+ db, items=items_data, remark=body.remark,
|
|
|
+ tenant_no=tenant_no, user_id=user.get("user_id"),
|
|
|
+ )
|
|
|
+ return success_response({"count": count}, message=f"成功入库 {count} 条")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/consumables/batch-stock-out")
|
|
|
+async def consumable_batch_stock_out(
|
|
|
+ body: BatchStockOutBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items_data = [it.model_dump() for it in body.items]
|
|
|
+ count = consumable_svc.batch_stock_out(
|
|
|
+ db, items=items_data,
|
|
|
+ receiver=body.receiver, purpose=body.purpose, remark=body.remark,
|
|
|
+ tenant_no=tenant_no, user_id=user.get("user_id"),
|
|
|
+ )
|
|
|
+ return success_response({"count": count}, message=f"成功出库 {count} 条")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/consumables/{consumable_id}/stock-logs")
|
|
|
+async def list_consumable_stock_logs(
|
|
|
+ consumable_id: str,
|
|
|
+ page: int = Query(1, ge=1),
|
|
|
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
|
+ changeType: Optional[str] = None,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items, total = consumable_stock_log_svc.list_stock_logs(
|
|
|
+ db, tenant_no, page=page, page_size=pageSize,
|
|
|
+ material_id=consumable_id, change_type=changeType,
|
|
|
+ user_info=user,
|
|
|
+ )
|
|
|
+ tp = (total + pageSize - 1) // pageSize if total > 0 else 0
|
|
|
+ return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/consumables/stock-logs")
|
|
|
+async def list_all_consumable_stock_logs(
|
|
|
+ page: int = Query(1, ge=1),
|
|
|
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
|
+ keyword: Optional[str] = None,
|
|
|
+ changeType: Optional[str] = None,
|
|
|
+ startTime: Optional[str] = None,
|
|
|
+ endTime: Optional[str] = None,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items, total = consumable_stock_log_svc.list_stock_logs(
|
|
|
+ db, tenant_no, page=page, page_size=pageSize,
|
|
|
+ keyword=keyword, change_type=changeType,
|
|
|
+ start_time=startTime, end_time=endTime,
|
|
|
+ user_info=user,
|
|
|
+ )
|
|
|
+ tp = (total + pageSize - 1) // pageSize if total > 0 else 0
|
|
|
+ return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
+
|
|
|
+
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# 2. 试剂
|
|
|
# ---------------------------------------------------------------------------
|
|
|
@@ -297,6 +464,130 @@ async def delete_reagent(
|
|
|
return error_response(400, str(e))
|
|
|
|
|
|
|
|
|
+@router.post("/reagents/{reagent_id}/stock-in")
|
|
|
+async def reagent_stock_in(
|
|
|
+ reagent_id: str,
|
|
|
+ body: StockInBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ from decimal import Decimal
|
|
|
+ obj = reagent_svc.stock_in(
|
|
|
+ db, reagent_id,
|
|
|
+ qty=Decimal(str(body.qty)),
|
|
|
+ remark=body.remark,
|
|
|
+ user_id=user.get("user_id"),
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ )
|
|
|
+ return success_response(reagent_svc._row_to_dict(obj), message="入库成功")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/reagents/{reagent_id}/stock-out")
|
|
|
+async def reagent_stock_out(
|
|
|
+ reagent_id: str,
|
|
|
+ body: StockOutBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ from decimal import Decimal
|
|
|
+ obj = reagent_svc.stock_out(
|
|
|
+ db, reagent_id,
|
|
|
+ qty=Decimal(str(body.qty)),
|
|
|
+ receiver=body.receiver,
|
|
|
+ purpose=body.purpose,
|
|
|
+ remark=body.remark,
|
|
|
+ user_id=user.get("user_id"),
|
|
|
+ tenant_no=tenant_no,
|
|
|
+ )
|
|
|
+ return success_response(reagent_svc._row_to_dict(obj), message="出库成功")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/reagents/batch-stock-in")
|
|
|
+async def reagent_batch_stock_in(
|
|
|
+ body: BatchStockInBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items_data = [it.model_dump() for it in body.items]
|
|
|
+ count = reagent_svc.batch_stock_in(
|
|
|
+ db, items=items_data, remark=body.remark,
|
|
|
+ tenant_no=tenant_no, user_id=user.get("user_id"),
|
|
|
+ )
|
|
|
+ return success_response({"count": count}, message=f"成功入库 {count} 条")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/reagents/batch-stock-out")
|
|
|
+async def reagent_batch_stock_out(
|
|
|
+ body: BatchStockOutBody,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ try:
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items_data = [it.model_dump() for it in body.items]
|
|
|
+ count = reagent_svc.batch_stock_out(
|
|
|
+ db, items=items_data,
|
|
|
+ receiver=body.receiver, purpose=body.purpose, remark=body.remark,
|
|
|
+ tenant_no=tenant_no, user_id=user.get("user_id"),
|
|
|
+ )
|
|
|
+ return success_response({"count": count}, message=f"成功出库 {count} 条")
|
|
|
+ except ValueError as e:
|
|
|
+ return error_response(400, str(e))
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/reagents/{reagent_id}/stock-logs")
|
|
|
+async def list_reagent_stock_logs(
|
|
|
+ reagent_id: str,
|
|
|
+ page: int = Query(1, ge=1),
|
|
|
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
|
+ changeType: Optional[str] = None,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items, total = reagent_stock_log_svc.list_stock_logs(
|
|
|
+ db, tenant_no, page=page, page_size=pageSize,
|
|
|
+ material_id=reagent_id, change_type=changeType,
|
|
|
+ user_info=user,
|
|
|
+ )
|
|
|
+ tp = (total + pageSize - 1) // pageSize if total > 0 else 0
|
|
|
+ return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/reagents/stock-logs")
|
|
|
+async def list_all_reagent_stock_logs(
|
|
|
+ page: int = Query(1, ge=1),
|
|
|
+ pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
|
+ keyword: Optional[str] = None,
|
|
|
+ changeType: Optional[str] = None,
|
|
|
+ startTime: Optional[str] = None,
|
|
|
+ endTime: Optional[str] = None,
|
|
|
+ user: dict = Depends(require_admin_or_super_admin),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ tenant_no = _get_tenant_no(db, user)
|
|
|
+ items, total = reagent_stock_log_svc.list_stock_logs(
|
|
|
+ db, tenant_no, page=page, page_size=pageSize,
|
|
|
+ keyword=keyword, change_type=changeType,
|
|
|
+ start_time=startTime, end_time=endTime,
|
|
|
+ user_info=user,
|
|
|
+ )
|
|
|
+ tp = (total + pageSize - 1) // pageSize if total > 0 else 0
|
|
|
+ return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
+
|
|
|
+
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# 3. SOP文档
|
|
|
# ---------------------------------------------------------------------------
|