落库并展示质检工单级结论(可编辑/任务派生),库位树支持批量新增子节点与货架格子检索,协议/捐赠列表补充接收与门禁质检摘要,并同步生产物料与工单卡片展示优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
1107 lines
41 KiB
Python
1107 lines
41 KiB
Python
"""运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约)"""
|
|
from datetime import datetime
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.data_scope import apply_data_scope
|
|
from app.core.database import get_db
|
|
from app.core.menu_guard import require_admin_or_super_admin
|
|
from app.core.response import error_response, success_response
|
|
from app.crud import clean_room as clean_room_svc
|
|
from app.crud import exhibition_appointment as exhibition_svc
|
|
from app.crud import storage_facility as storage_facility_svc
|
|
from app.crud import storage_occupancy as occupancy_svc
|
|
from app.core.utils import generate_uuid
|
|
from app.models.models import BizCleanRoom, BizCleanRoomAccessLog, BizCleanRoomUvDisinfectLog, BizExhibitionAppointment, BizStorageFacility, BizStorageFacilitySlot, SysUser
|
|
|
|
router = APIRouter(prefix="/web/admin", tags=["运营支撑"])
|
|
|
|
|
|
def _get_tenant_no(db: Session, user: dict) -> str:
|
|
user_id = user.get("user_id")
|
|
if not user_id:
|
|
raise PermissionError("用户身份信息缺失,无法确定租户")
|
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
|
if not sys_user or not sys_user.tenant_no:
|
|
raise PermissionError("用户租户信息缺失")
|
|
return sys_user.tenant_no
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 洁净室
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class CleanRoomCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
room_code: str = Field(..., alias="roomCode")
|
|
room_name: str = Field(..., alias="roomName")
|
|
room_grade: Optional[str] = Field(None, alias="roomGrade")
|
|
room_status: Optional[str] = Field("zc", alias="roomStatus")
|
|
area_sqm: Optional[float] = Field(None, alias="areaSqm")
|
|
room_image: Optional[str] = Field(None, alias="roomImage")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class CleanRoomUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
room_code: Optional[str] = Field(None, alias="roomCode")
|
|
room_name: Optional[str] = Field(None, alias="roomName")
|
|
room_grade: Optional[str] = Field(None, alias="roomGrade")
|
|
room_status: Optional[str] = Field(None, alias="roomStatus")
|
|
area_sqm: Optional[float] = Field(None, alias="areaSqm")
|
|
room_image: Optional[str] = Field(None, alias="roomImage")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/clean-rooms")
|
|
async def list_clean_rooms(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
keyword: Optional[str] = None,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizCleanRoom).filter(
|
|
BizCleanRoom.del_flag == 0,
|
|
BizCleanRoom.tenant_no == tenant_no,
|
|
)
|
|
kw = (keyword or "").strip()
|
|
if kw:
|
|
like = f"%{kw}%"
|
|
query = query.filter(or_(BizCleanRoom.room_code.like(like), BizCleanRoom.room_name.like(like)))
|
|
query = apply_data_scope(query, BizCleanRoom, user, db, tenant_no)
|
|
total = query.count()
|
|
items = query.order_by(BizCleanRoom.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
|
|
result_list = [clean_room_svc._row_to_dict(r) for r in items]
|
|
tp = clean_room_svc.total_pages(total, pageSize)
|
|
return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
|
|
|
@router.post("/clean-rooms")
|
|
async def create_clean_room(
|
|
body: CleanRoomCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = clean_room_svc.create_clean_room(
|
|
db,
|
|
room_code=body.room_code,
|
|
room_name=body.room_name,
|
|
room_grade=body.room_grade,
|
|
room_status=body.room_status or "zc",
|
|
area_sqm=body.area_sqm,
|
|
room_image=body.room_image,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(clean_room_svc._row_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/clean-rooms/{room_id}")
|
|
async def update_clean_room(
|
|
room_id: str,
|
|
body: CleanRoomUpdateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
obj = clean_room_svc.update_clean_room(
|
|
db,
|
|
room_id,
|
|
room_code=payload.get("room_code"),
|
|
room_name=payload.get("room_name"),
|
|
room_grade=payload.get("room_grade"),
|
|
room_status=payload.get("room_status"),
|
|
area_sqm=payload.get("area_sqm"),
|
|
room_image=payload.get("room_image"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id=user.get("user_id"),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(clean_room_svc._row_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/clean-rooms/{room_id}")
|
|
async def delete_clean_room(
|
|
room_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
clean_room_svc.soft_delete_clean_room(db, room_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 进出记录
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class AccessLogCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
clean_room_id: str = Field(..., alias="cleanRoomId")
|
|
person_name: Optional[str] = Field(None, alias="personName")
|
|
dept_name: Optional[str] = Field(None, alias="deptName")
|
|
enter_time: Optional[str] = Field(None, alias="enterTime")
|
|
purpose: Optional[str] = None
|
|
leave_time: Optional[str] = Field(None, alias="leaveTime")
|
|
body_temp: Optional[str] = Field(None, alias="bodyTemp")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class AccessLogUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
person_name: Optional[str] = Field(None, alias="personName")
|
|
dept_name: Optional[str] = Field(None, alias="deptName")
|
|
enter_time: Optional[str] = Field(None, alias="enterTime")
|
|
purpose: Optional[str] = None
|
|
leave_time: Optional[str] = Field(None, alias="leaveTime")
|
|
body_temp: Optional[str] = Field(None, alias="bodyTemp")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
def _parse_optional_dt(value: Optional[str]) -> Optional[datetime]:
|
|
if not value:
|
|
return None
|
|
return datetime.fromisoformat(value)
|
|
|
|
|
|
@router.get("/clean-room-access-logs")
|
|
async def list_access_logs(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
cleanRoomId: str = Query(..., alias="cleanRoomId"),
|
|
keyword: Optional[str] = None,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizCleanRoomAccessLog).filter(
|
|
BizCleanRoomAccessLog.del_flag == 0,
|
|
BizCleanRoomAccessLog.tenant_no == tenant_no,
|
|
BizCleanRoomAccessLog.clean_room_id == cleanRoomId,
|
|
)
|
|
if keyword:
|
|
kw = f"%{keyword}%"
|
|
query = query.filter(
|
|
BizCleanRoomAccessLog.person_name.like(kw)
|
|
| BizCleanRoomAccessLog.dept_name.like(kw)
|
|
)
|
|
query = apply_data_scope(query, BizCleanRoomAccessLog, user, db, tenant_no)
|
|
total = query.count()
|
|
items = query.order_by(BizCleanRoomAccessLog.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
|
|
result_list = [clean_room_svc._access_log_to_dict(r) for r in items]
|
|
tp = clean_room_svc.total_pages(total, pageSize)
|
|
return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
|
|
|
@router.post("/clean-room-access-logs")
|
|
async def create_access_log(
|
|
body: AccessLogCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = clean_room_svc.create_access_log(
|
|
db,
|
|
clean_room_id=body.clean_room_id,
|
|
person_name=body.person_name,
|
|
dept_name=body.dept_name,
|
|
enter_time=_parse_optional_dt(body.enter_time),
|
|
purpose=body.purpose,
|
|
leave_time=_parse_optional_dt(body.leave_time),
|
|
body_temp=body.body_temp,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id_param=user.get("user_id"),
|
|
)
|
|
return success_response(clean_room_svc._access_log_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/clean-room-access-logs/{log_id}")
|
|
async def update_access_log(
|
|
log_id: str,
|
|
body: AccessLogUpdateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
obj = clean_room_svc.update_access_log(
|
|
db,
|
|
log_id,
|
|
person_name=payload.get("person_name"),
|
|
dept_name=payload.get("dept_name"),
|
|
enter_time=_parse_optional_dt(payload.get("enter_time")) if "enter_time" in payload else None,
|
|
purpose=payload.get("purpose"),
|
|
leave_time=_parse_optional_dt(payload.get("leave_time")) if "leave_time" in payload else None,
|
|
body_temp=payload.get("body_temp"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id_param=user.get("user_id"),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(clean_room_svc._access_log_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/clean-room-access-logs/{log_id}")
|
|
async def delete_access_log(
|
|
log_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
clean_room_svc.soft_delete_access_log(db, log_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 紫外线消毒记录
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class UvDisinfectLogCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
clean_room_id: str = Field(..., alias="cleanRoomId")
|
|
operator_name: Optional[str] = Field(None, alias="operatorName")
|
|
start_time: Optional[str] = Field(None, alias="startTime")
|
|
end_time: Optional[str] = Field(None, alias="endTime")
|
|
duration_minutes: Optional[int] = Field(None, alias="durationMinutes")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class UvDisinfectLogUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
operator_name: Optional[str] = Field(None, alias="operatorName")
|
|
start_time: Optional[str] = Field(None, alias="startTime")
|
|
end_time: Optional[str] = Field(None, alias="endTime")
|
|
duration_minutes: Optional[int] = Field(None, alias="durationMinutes")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/clean-room-uv-disinfect-logs")
|
|
async def list_uv_disinfect_logs(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
cleanRoomId: str = Query(..., alias="cleanRoomId"),
|
|
keyword: Optional[str] = None,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizCleanRoomUvDisinfectLog).filter(
|
|
BizCleanRoomUvDisinfectLog.del_flag == 0,
|
|
BizCleanRoomUvDisinfectLog.tenant_no == tenant_no,
|
|
BizCleanRoomUvDisinfectLog.clean_room_id == cleanRoomId,
|
|
)
|
|
if keyword:
|
|
kw = f"%{keyword}%"
|
|
query = query.filter(BizCleanRoomUvDisinfectLog.operator_name.like(kw))
|
|
query = apply_data_scope(query, BizCleanRoomUvDisinfectLog, user, db, tenant_no)
|
|
total = query.count()
|
|
items = (
|
|
query.order_by(BizCleanRoomUvDisinfectLog.create_time.desc())
|
|
.offset((page - 1) * pageSize)
|
|
.limit(pageSize)
|
|
.all()
|
|
)
|
|
result_list = [clean_room_svc._uv_disinfect_log_to_dict(r) for r in items]
|
|
tp = clean_room_svc.total_pages(total, pageSize)
|
|
return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
|
|
|
@router.post("/clean-room-uv-disinfect-logs")
|
|
async def create_uv_disinfect_log(
|
|
body: UvDisinfectLogCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = clean_room_svc.create_uv_disinfect_log(
|
|
db,
|
|
clean_room_id=body.clean_room_id,
|
|
operator_name=body.operator_name,
|
|
start_time=_parse_optional_dt(body.start_time),
|
|
end_time=_parse_optional_dt(body.end_time),
|
|
duration_minutes=body.duration_minutes,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id_param=user.get("user_id"),
|
|
)
|
|
return success_response(clean_room_svc._uv_disinfect_log_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/clean-room-uv-disinfect-logs/{log_id}")
|
|
async def update_uv_disinfect_log(
|
|
log_id: str,
|
|
body: UvDisinfectLogUpdateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
obj = clean_room_svc.update_uv_disinfect_log(
|
|
db,
|
|
log_id,
|
|
operator_name=payload.get("operator_name"),
|
|
start_time=_parse_optional_dt(payload.get("start_time")) if "start_time" in payload else None,
|
|
end_time=_parse_optional_dt(payload.get("end_time")) if "end_time" in payload else None,
|
|
duration_minutes=payload.get("duration_minutes"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id_param=user.get("user_id"),
|
|
tenant_no=tenant_no,
|
|
clear_end_time="end_time" in payload and payload.get("end_time") is None,
|
|
)
|
|
return success_response(clean_room_svc._uv_disinfect_log_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/clean-room-uv-disinfect-logs/{log_id}")
|
|
async def delete_uv_disinfect_log(
|
|
log_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
clean_room_svc.soft_delete_uv_disinfect_log(db, log_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 仓储设施
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class StorageFacilityCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
parent_id: str = Field("0", alias="parentId")
|
|
node_name: str = Field(..., alias="nodeName")
|
|
node_code: str = Field("", alias="nodeCode", max_length=32)
|
|
node_type: Optional[str] = Field(None, alias="nodeType")
|
|
is_leaf: Optional[int] = Field(0, alias="isLeaf")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StorageFacilityUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
node_name: Optional[str] = Field(None, alias="nodeName")
|
|
node_code: Optional[str] = Field(None, alias="nodeCode", max_length=32)
|
|
node_type: Optional[str] = Field(None, alias="nodeType")
|
|
is_leaf: Optional[int] = Field(None, alias="isLeaf")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StorageFacilityBatchCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
parent_id: str = Field("0", alias="parentId")
|
|
name_prefix: str = Field("", alias="namePrefix")
|
|
name_suffix: str = Field("", alias="nameSuffix")
|
|
start_num: int = Field(..., alias="startNum", ge=0)
|
|
end_num: int = Field(..., alias="endNum", ge=0)
|
|
pad_width: int = Field(0, alias="padWidth", ge=0, le=8)
|
|
code_prefix: str = Field("", alias="codePrefix")
|
|
code_suffix: str = Field("", alias="codeSuffix")
|
|
node_type: Optional[str] = Field(None, alias="nodeType")
|
|
is_leaf: Optional[int] = Field(0, alias="isLeaf")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/storage-facilities")
|
|
async def list_storage_facilities(
|
|
keyword: Optional[str] = None,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizStorageFacility).filter(
|
|
BizStorageFacility.del_flag == 0,
|
|
BizStorageFacility.tenant_no == tenant_no,
|
|
)
|
|
if keyword:
|
|
like = f"%{keyword}%"
|
|
query = query.filter(
|
|
or_(
|
|
BizStorageFacility.node_name.like(like),
|
|
BizStorageFacility.node_code.like(like),
|
|
)
|
|
)
|
|
query = apply_data_scope(query, BizStorageFacility, user, db, tenant_no)
|
|
items = query.order_by(
|
|
BizStorageFacility.order_num.asc(),
|
|
BizStorageFacility.create_time.asc(),
|
|
).all()
|
|
result_list = [storage_facility_svc._row_to_dict(r) for r in items]
|
|
return success_response(result_list)
|
|
|
|
|
|
@router.post("/storage-facilities")
|
|
async def create_storage_facility(
|
|
body: StorageFacilityCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = storage_facility_svc.create_facility(
|
|
db,
|
|
parent_id=body.parent_id,
|
|
node_name=body.node_name,
|
|
node_code=body.node_code,
|
|
node_type=body.node_type,
|
|
is_leaf=body.is_leaf,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(storage_facility_svc._row_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/storage-facilities/batch")
|
|
async def batch_create_storage_facilities(
|
|
body: StorageFacilityBatchCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = storage_facility_svc.batch_create_facilities(
|
|
db,
|
|
parent_id=body.parent_id,
|
|
name_prefix=body.name_prefix,
|
|
name_suffix=body.name_suffix,
|
|
start_num=body.start_num,
|
|
end_num=body.end_num,
|
|
pad_width=body.pad_width,
|
|
code_prefix=body.code_prefix,
|
|
code_suffix=body.code_suffix,
|
|
node_type=body.node_type,
|
|
is_leaf=body.is_leaf or 0,
|
|
tenant_no=tenant_no,
|
|
remark=body.remark,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(items, message=f"批量创建 {len(items)} 个子节点成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/storage-facilities/{node_id}")
|
|
async def update_storage_facility(
|
|
node_id: str,
|
|
body: StorageFacilityUpdateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
obj = storage_facility_svc.update_facility(
|
|
db,
|
|
node_id,
|
|
node_name=payload.get("node_name"),
|
|
node_code=payload.get("node_code"),
|
|
node_type=payload.get("node_type"),
|
|
is_leaf=payload.get("is_leaf"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id=user.get("user_id"),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(storage_facility_svc._row_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/storage-facilities/{node_id}")
|
|
async def delete_storage_facility(
|
|
node_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
storage_facility_svc.soft_delete_facility(db, node_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 储位
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class StorageSlotCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
facility_node_id: str = Field(..., alias="facilityNodeId")
|
|
slot_code: Optional[str] = Field(None, alias="slotCode")
|
|
slot_status: Optional[str] = Field(None, alias="slotStatus")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
at_row_num: Optional[int] = Field(None, alias="atRowNum", ge=1)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StorageSlotBatchCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
facility_node_id: str = Field(..., alias="facilityNodeId")
|
|
rows: int = Field(..., ge=1, le=50)
|
|
cols: int = Field(..., ge=1, le=50)
|
|
|
|
|
|
class StorageSlotUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
slot_code: Optional[str] = Field(None, alias="slotCode")
|
|
slot_status: Optional[str] = Field(None, alias="slotStatus")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StorageSlotClearBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
facility_node_id: str = Field(..., alias="facilityNodeId")
|
|
|
|
|
|
class StorageSlotInsertRowBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
facility_node_id: str = Field(..., alias="facilityNodeId")
|
|
after_row_num: int = Field(..., ge=0, alias="afterRowNum")
|
|
cols: int = Field(..., ge=1)
|
|
|
|
|
|
@router.get("/storage-facility-slots")
|
|
async def list_storage_slots(
|
|
facilityNodeId: str = Query(..., alias="facilityNodeId"),
|
|
availableOnly: Optional[bool] = Query(None, alias="availableOnly"),
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizStorageFacilitySlot).filter(
|
|
BizStorageFacilitySlot.del_flag == 0,
|
|
BizStorageFacilitySlot.tenant_no == tenant_no,
|
|
BizStorageFacilitySlot.facility_node_id == facilityNodeId,
|
|
)
|
|
query = apply_data_scope(query, BizStorageFacilitySlot, user, db, tenant_no)
|
|
items = query.order_by(
|
|
BizStorageFacilitySlot.order_num.asc(),
|
|
BizStorageFacilitySlot.create_time.asc(),
|
|
).all()
|
|
facility_by_id = storage_facility_svc.load_facility_ancestor_map(
|
|
db, tenant_no, facilityNodeId
|
|
)
|
|
result_list = [storage_facility_svc._slot_to_dict(r, facility_by_id) for r in items]
|
|
if availableOnly:
|
|
# 仅空格场景无需 enrich 在库产品
|
|
result_list = [
|
|
row for row in result_list
|
|
if (row.get("slotStatus") or "") == "空" or int(row.get("occupiedCount") or 0) == 0
|
|
]
|
|
else:
|
|
# 占用格附带名称/类型(轻量批量,供库位管理货架展示)
|
|
result_list = occupancy_svc.enrich_slots_with_stored_cell_products(
|
|
db, tenant_no, result_list
|
|
)
|
|
return success_response(result_list)
|
|
|
|
|
|
@router.get("/storage-facility-slots/stats")
|
|
async def list_storage_slot_stats(
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""各叶子节点格子占用/总数统计(单次聚合,替代逐节点拉全量格子列表)。"""
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizStorageFacilitySlot).filter(
|
|
BizStorageFacilitySlot.del_flag == 0,
|
|
BizStorageFacilitySlot.tenant_no == tenant_no,
|
|
)
|
|
query = apply_data_scope(query, BizStorageFacilitySlot, user, db, tenant_no)
|
|
items = storage_facility_svc.aggregate_slot_stats_by_facility(query=query)
|
|
return success_response(items)
|
|
|
|
|
|
@router.get("/storage-facility-slots/select-options")
|
|
async def list_storage_slot_select_options(
|
|
keyword: Optional[str] = None,
|
|
availableOnly: Optional[bool] = Query(None, alias="availableOnly"),
|
|
includeSlotIds: Optional[str] = Query(None, alias="includeSlotIds"),
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(20, ge=1, le=50, alias="pageSize"),
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizStorageFacilitySlot).filter(
|
|
BizStorageFacilitySlot.del_flag == 0,
|
|
BizStorageFacilitySlot.tenant_no == tenant_no,
|
|
)
|
|
query = apply_data_scope(query, BizStorageFacilitySlot, user, db, tenant_no)
|
|
include_ids = [
|
|
x.strip()
|
|
for x in (includeSlotIds or "").split(",")
|
|
if x.strip()
|
|
]
|
|
items, total = storage_facility_svc.list_slot_select_options(
|
|
db,
|
|
query=query,
|
|
tenant_no=tenant_no,
|
|
keyword=keyword,
|
|
available_only=bool(availableOnly),
|
|
include_slot_ids=include_ids or None,
|
|
page=page,
|
|
page_size=pageSize,
|
|
)
|
|
return success_response(
|
|
{
|
|
"list": items,
|
|
"total": total,
|
|
"page": page,
|
|
"pageSize": pageSize,
|
|
"totalPages": (total + pageSize - 1) // pageSize if pageSize else 0,
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/storage-facility-slots/batch")
|
|
async def batch_create_storage_slots(
|
|
body: StorageSlotBatchCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = storage_facility_svc.batch_create_slots(
|
|
db,
|
|
facility_node_id=body.facility_node_id,
|
|
rows=body.rows,
|
|
cols=body.cols,
|
|
tenant_no=tenant_no,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(items, message=f"批量创建 {len(items)} 个格子成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/storage-facility-slots")
|
|
async def create_storage_slot(
|
|
body: StorageSlotCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = storage_facility_svc.create_slot(
|
|
db,
|
|
facility_node_id=body.facility_node_id,
|
|
slot_code=body.slot_code,
|
|
slot_status=body.slot_status,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=user.get("user_id"),
|
|
at_row_num=body.at_row_num,
|
|
)
|
|
return success_response(storage_facility_svc._slot_to_dict_for_row(db, obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/storage-facility-slots/{slot_id}")
|
|
async def update_storage_slot(
|
|
slot_id: str,
|
|
body: StorageSlotUpdateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
obj = storage_facility_svc.update_slot(
|
|
db,
|
|
slot_id,
|
|
slot_code=payload.get("slot_code"),
|
|
slot_status=payload.get("slot_status"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id=user.get("user_id"),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(storage_facility_svc._slot_to_dict_for_row(db, obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/storage-facility-slots/insert-row")
|
|
async def insert_storage_slot_row(
|
|
body: StorageSlotInsertRowBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
result = storage_facility_svc.insert_row_and_shift_slots(
|
|
db,
|
|
facility_node_id=body.facility_node_id,
|
|
after_row_num=body.after_row_num,
|
|
cols=body.cols,
|
|
tenant_no=tenant_no,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(result, message="新增行成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/storage-facility-slots/clear")
|
|
async def clear_storage_slots(
|
|
body: StorageSlotClearBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
cleared_count = storage_facility_svc.clear_slots_by_facility_node(
|
|
db,
|
|
facility_node_id=body.facility_node_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(
|
|
{"clearedCount": cleared_count},
|
|
message=f"已清空 {cleared_count} 个格子",
|
|
)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/storage-facility-slots/{slot_id}")
|
|
async def delete_storage_slot(
|
|
slot_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
storage_facility_svc.soft_delete_slot(
|
|
db, slot_id, tenant_no=tenant_no, user_id=user.get("user_id")
|
|
)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 库位占用记录
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class OccupancyCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
slot_id: str = Field(..., alias="slotId")
|
|
item_type: str = Field("other", alias="itemType")
|
|
item_code: str = Field(..., alias="itemCode")
|
|
item_name: str = Field(..., alias="itemName")
|
|
item_source: str = Field("manual", alias="itemSource")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/storage-slot-occupancies")
|
|
async def list_occupancies(
|
|
slotId: str = Query(..., alias="slotId"),
|
|
occupancyStatus: Optional[str] = Query("stored", alias="occupancyStatus"),
|
|
itemType: Optional[str] = Query(None, alias="itemType"),
|
|
page: int = Query(1),
|
|
pageSize: int = Query(20, alias="pageSize"),
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
result = occupancy_svc.list_occupancies(
|
|
db,
|
|
slot_id=slotId,
|
|
occupancy_status=occupancyStatus,
|
|
item_type=itemType,
|
|
tenant_no=tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
)
|
|
return success_response(result)
|
|
|
|
|
|
@router.get("/storage-slot-occupancies/history")
|
|
async def list_occupancy_history(
|
|
slotId: Optional[str] = Query(None, alias="slotId"),
|
|
facilityNodeId: Optional[str] = Query(None, alias="facilityNodeId"),
|
|
itemType: Optional[str] = Query(None, alias="itemType"),
|
|
startDate: Optional[str] = Query(None, alias="startDate"),
|
|
endDate: Optional[str] = Query(None, alias="endDate"),
|
|
page: int = Query(1),
|
|
pageSize: int = Query(20, alias="pageSize"),
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
result = occupancy_svc.list_occupancy_history(
|
|
db,
|
|
slot_id=slotId,
|
|
facility_node_id=facilityNodeId,
|
|
item_type=itemType,
|
|
start_date=startDate,
|
|
end_date=endDate,
|
|
tenant_no=tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
)
|
|
return success_response(result)
|
|
|
|
|
|
@router.post("/storage-slot-occupancies")
|
|
async def create_occupancy(
|
|
body: OccupancyCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
item_id = generate_uuid()
|
|
obj = occupancy_svc.create_occupancy(
|
|
db,
|
|
slot_id=body.slot_id,
|
|
item_type=body.item_type,
|
|
item_id=item_id,
|
|
item_code=body.item_code,
|
|
item_name=body.item_name,
|
|
item_source=body.item_source,
|
|
tenant_no=tenant_no,
|
|
user_id=user.get("user_id"),
|
|
remark=body.remark,
|
|
)
|
|
return success_response(occupancy_svc._row_to_dict(obj), message="入库成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/storage-slot-occupancies/{occupancy_id}/remove")
|
|
async def remove_occupancy(
|
|
occupancy_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
obj = occupancy_svc.remove_occupancy(
|
|
db,
|
|
occupancy_id,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(occupancy_svc._row_to_dict(obj), message="出库成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 展厅预约
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ExhibitionAppointmentCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
appointment_no: Optional[str] = Field(None, alias="appointmentNo")
|
|
visit_org: Optional[str] = Field(None, alias="visitOrg")
|
|
contact_name: Optional[str] = Field(None, alias="contactName")
|
|
visit_date: Optional[str] = Field(None, alias="visitDate")
|
|
visitor_count: Optional[int] = Field(None, alias="visitorCount")
|
|
reception_user_id: Optional[str] = Field(None, alias="receptionUserId")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class ExhibitionAppointmentUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
visit_org: Optional[str] = Field(None, alias="visitOrg")
|
|
contact_name: Optional[str] = Field(None, alias="contactName")
|
|
visit_date: Optional[str] = Field(None, alias="visitDate")
|
|
visitor_count: Optional[int] = Field(None, alias="visitorCount")
|
|
reception_user_id: Optional[str] = Field(None, alias="receptionUserId")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
def _normalize_visit_date_bound(value: str, *, end_of_day: bool = False) -> str:
|
|
val = (value or "").strip()
|
|
if len(val) == 10:
|
|
return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
|
|
return val
|
|
|
|
|
|
@router.get("/exhibition-appointments")
|
|
async def list_exhibition_appointments(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
|
|
keyword: Optional[str] = None,
|
|
visitDateStart: Optional[str] = Query(None, alias="visitDateStart"),
|
|
visitDateEnd: Optional[str] = Query(None, alias="visitDateEnd"),
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
query = db.query(BizExhibitionAppointment).filter(
|
|
BizExhibitionAppointment.del_flag == 0,
|
|
BizExhibitionAppointment.tenant_no == tenant_no,
|
|
)
|
|
kw = (keyword or "").strip()
|
|
if kw:
|
|
like = f"%{kw}%"
|
|
query = query.filter(
|
|
or_(
|
|
BizExhibitionAppointment.appointment_no.like(like),
|
|
BizExhibitionAppointment.contact_name.like(like),
|
|
BizExhibitionAppointment.visit_org.like(like),
|
|
)
|
|
)
|
|
if visitDateStart:
|
|
query = query.filter(
|
|
BizExhibitionAppointment.visit_date >= _normalize_visit_date_bound(visitDateStart)
|
|
)
|
|
if visitDateEnd:
|
|
query = query.filter(
|
|
BizExhibitionAppointment.visit_date <= _normalize_visit_date_bound(visitDateEnd, end_of_day=True)
|
|
)
|
|
query = apply_data_scope(query, BizExhibitionAppointment, user, db, tenant_no)
|
|
total = query.count()
|
|
items = query.order_by(BizExhibitionAppointment.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
|
|
result_list = [exhibition_svc._row_to_dict(r) for r in items]
|
|
tp = exhibition_svc.total_pages(total, pageSize)
|
|
return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
|
|
|
@router.post("/exhibition-appointments")
|
|
async def create_exhibition_appointment(
|
|
body: ExhibitionAppointmentCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = exhibition_svc.create_exhibition_appointment(
|
|
db,
|
|
appointment_no=body.appointment_no,
|
|
visit_org=body.visit_org,
|
|
contact_name=body.contact_name,
|
|
visit_date=body.visit_date,
|
|
visitor_count=body.visitor_count,
|
|
reception_user_id=body.reception_user_id,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(exhibition_svc._row_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/exhibition-appointments/{appointment_id}")
|
|
async def update_exhibition_appointment(
|
|
appointment_id: str,
|
|
body: ExhibitionAppointmentUpdateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
obj = exhibition_svc.update_exhibition_appointment(
|
|
db,
|
|
appointment_id,
|
|
visit_org=payload.get("visit_org"),
|
|
contact_name=payload.get("contact_name"),
|
|
visit_date=payload.get("visit_date"),
|
|
visitor_count=payload.get("visitor_count"),
|
|
reception_user_id=payload.get("reception_user_id"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(exhibition_svc._row_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/exhibition-appointments/{appointment_id}")
|
|
async def delete_exhibition_appointment(
|
|
appointment_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
exhibition_svc.soft_delete_exhibition_appointment(db, appointment_id)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|