| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- """运输计划管理 - 盘存统计 API"""
- from datetime import date
- from decimal import Decimal
- from typing import Optional
- from fastapi import APIRouter, Depends, HTTPException, Query
- from sqlalchemy import func
- from sqlalchemy.orm import Session
- from app.core.database import get_db
- from app.core.db_auth import DbUserContext, get_db_current_user
- from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
- from app.core.tenant_context import get_tenant_no
- from app.core.utils import generate_uuid
- from app.models.pl_models import (
- PlDocumentReminder,
- PlInventory,
- PlInventoryTransaction,
- PlInventoryWarning,
- )
- from app.schemas.pl.inventory import (
- DocumentReminderRead,
- DocumentReminderStatsRead,
- DocumentReminderUpdate,
- InventoryLedgerQuery,
- InventoryLedgerResponse,
- InventoryRead,
- InventorySummaryItem,
- InventorySummaryResponse,
- InventoryTransactionQuery,
- InventoryTransactionRead,
- InventoryTransactionResponse,
- InventoryWarningRead,
- InventoryWarningThreshold,
- InventoryWarningUpdate,
- TransportStatsItem,
- TransportStatsResponse,
- )
- from app.services.pl_cert_service import scan_cert_expiry
- router = APIRouter(prefix="/inventory", tags=["运输计划-盘存统计"])
- # =============================================================================
- # 库存台账
- # =============================================================================
- @router.get("/ledger", response_model=InventoryLedgerResponse)
- def get_inventory_ledger(
- product_name: Optional[str] = Query(default=None, alias="productName", description="产品名称"),
- page: int = Query(default=1, ge=1),
- page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> InventoryLedgerResponse:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- query = db.query(PlInventory).filter(
- PlInventory.tenant_no == tenant_no,
- PlInventory.del_flag == 0,
- )
- if product_name:
- query = query.filter(PlInventory.product_name.contains(product_name))
- query = apply_pl_scope(query, PlInventory, user, db, tenant_no)
- total = query.count()
- offset = (page - 1) * page_size
- items = query.order_by(PlInventory.product_name).offset(offset).limit(page_size).all()
- return InventoryLedgerResponse(total=total, list=items)
- # =============================================================================
- # 库存预警阈值设置
- # =============================================================================
- @router.post("/warning-threshold")
- def set_warning_threshold(
- payload: InventoryWarningThreshold,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> dict:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- brand_id = payload.brand_id or ""
- inventory = (
- db.query(PlInventory)
- .filter(
- PlInventory.product_id == payload.product_id,
- PlInventory.brand_id == brand_id,
- PlInventory.tenant_no == tenant_no,
- PlInventory.del_flag == 0,
- )
- .first()
- )
- if inventory:
- assert_pl_row_in_scope(db, inventory, PlInventory, user, tenant_no)
- inventory.low_stock_warning = payload.low_stock_warning
- inventory.high_stock_warning = payload.high_stock_warning
- if payload.brand_name is not None:
- inventory.brand_name = payload.brand_name
- else:
- # 创建库存记录
- inventory = PlInventory(
- id=generate_uuid(),
- tenant_no=tenant_no,
- product_id=payload.product_id,
- brand_id=brand_id,
- brand_name=payload.brand_name,
- product_no="",
- product_name="",
- unit="吨",
- stock_qty=0,
- stock_amount=0,
- low_stock_warning=payload.low_stock_warning,
- high_stock_warning=payload.high_stock_warning,
- create_by=user.id,
- create_dept_id=user.dept_id,
- )
- db.add(inventory)
- db.commit()
- return {"success": True}
- # =============================================================================
- # 库存预警列表
- # =============================================================================
- @router.get("/warnings", response_model=list[InventoryWarningRead])
- def list_warnings(
- status: Optional[str] = Query(default=None, description="状态"),
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> list[InventoryWarningRead]:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- query = db.query(PlInventoryWarning).filter(
- PlInventoryWarning.tenant_no == tenant_no,
- PlInventoryWarning.del_flag == 0,
- )
- if status:
- query = query.filter(PlInventoryWarning.status == status)
- query = apply_pl_scope(query, PlInventoryWarning, user, db, tenant_no)
- return query.order_by(PlInventoryWarning.create_time.desc()).all()
- @router.put("/warnings/{id}", response_model=InventoryWarningRead)
- def update_warning(
- id: str,
- payload: InventoryWarningUpdate,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> InventoryWarningRead:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- warning = (
- db.query(PlInventoryWarning)
- .filter(
- PlInventoryWarning.id == id,
- PlInventoryWarning.tenant_no == tenant_no,
- PlInventoryWarning.del_flag == 0,
- )
- .first()
- )
- if not warning:
- raise HTTPException(status_code=404, detail="预警不存在")
- assert_pl_row_in_scope(db, warning, PlInventoryWarning, user, tenant_no)
- if payload.status == "resolved":
- warning.status = "resolved"
- from datetime import datetime
- warning.resolve_time = datetime.now()
- warning.resolve_remark = payload.resolve_remark
- db.commit()
- db.refresh(warning)
- return warning
- # =============================================================================
- # 出入库明细
- # =============================================================================
- @router.get("/transactions", response_model=InventoryTransactionResponse)
- def list_transactions(
- product_id: Optional[str] = Query(default=None, alias="productId", description="产品ID"),
- transaction_type: Optional[str] = Query(default=None, alias="transactionType", description="事务类型"),
- date_from: Optional[date] = Query(default=None, alias="dateFrom", description="开始日期"),
- date_to: Optional[date] = Query(default=None, alias="dateTo", description="结束日期"),
- page: int = Query(default=1, ge=1),
- page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> InventoryTransactionResponse:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- query = db.query(PlInventoryTransaction).filter(
- PlInventoryTransaction.tenant_no == tenant_no,
- PlInventoryTransaction.del_flag == 0,
- )
- if product_id:
- query = query.filter(PlInventoryTransaction.product_id == product_id)
- if transaction_type:
- query = query.filter(PlInventoryTransaction.transaction_type == transaction_type)
- if date_from:
- query = query.filter(PlInventoryTransaction.create_time >= date_from)
- if date_to:
- query = query.filter(PlInventoryTransaction.create_time <= date_to)
- query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)
- total = query.count()
- offset = (page - 1) * page_size
- items = query.order_by(PlInventoryTransaction.create_time.desc()).offset(offset).limit(page_size).all()
- return InventoryTransactionResponse(total=total, list=items)
- # =============================================================================
- # 出入库汇总
- # =============================================================================
- @router.get("/summary", response_model=InventorySummaryResponse)
- def get_summary(
- group_by: str = Query(default="month", description="汇总维度 month/week"),
- date_from: date = Query(..., alias="dateFrom", description="开始日期"),
- date_to: date = Query(..., alias="dateTo", description="结束日期"),
- product_id: Optional[str] = Query(default=None, alias="productId", description="产品ID"),
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> InventorySummaryResponse:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- query = db.query(PlInventoryTransaction).filter(
- PlInventoryTransaction.tenant_no == tenant_no,
- PlInventoryTransaction.del_flag == 0,
- PlInventoryTransaction.create_time >= date_from,
- PlInventoryTransaction.create_time <= date_to,
- )
- query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)
- if product_id:
- query = query.filter(PlInventoryTransaction.product_id == product_id)
- transactions = query.all()
- # 按产品分组汇总
- summary_map = {}
- for t in transactions:
- key = t.product_id
- if key not in summary_map:
- summary_map[key] = {
- "product_id": t.product_id,
- "product_name": t.product_name,
- "inbound_qty": Decimal("0"),
- "outbound_qty": Decimal("0"),
- }
- if t.transaction_type == "inbound":
- summary_map[key]["inbound_qty"] += t.qty
- else:
- summary_map[key]["outbound_qty"] += abs(t.qty)
- items = []
- for key, data in summary_map.items():
- net = data["inbound_qty"] - data["outbound_qty"]
- items.append(InventorySummaryItem(
- period="汇总",
- product_id=data["product_id"],
- product_name=data["product_name"],
- inbound_qty=data["inbound_qty"],
- outbound_qty=data["outbound_qty"],
- net_change=net,
- ))
- return InventorySummaryResponse(list=items)
- # =============================================================================
- # 证件到期提醒
- # =============================================================================
- @router.get("/document-reminders/stats", response_model=DocumentReminderStatsRead)
- def document_reminder_stats(
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> DocumentReminderStatsRead:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- scan_cert_expiry(db, tenant_no)
- db.commit()
- query = db.query(PlDocumentReminder).filter(
- PlDocumentReminder.tenant_no == tenant_no,
- PlDocumentReminder.del_flag == 0,
- PlDocumentReminder.status == "pending",
- PlDocumentReminder.days_remaining <= 0,
- )
- query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no)
- expired_pending_count = query.count()
- return DocumentReminderStatsRead(expired_pending_count=expired_pending_count)
- @router.get("/document-reminders", response_model=list[DocumentReminderRead])
- def list_document_reminders(
- status: Optional[str] = Query(default=None, description="状态"),
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> list[DocumentReminderRead]:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- scan_cert_expiry(db, tenant_no)
- db.commit()
- query = db.query(PlDocumentReminder).filter(
- PlDocumentReminder.tenant_no == tenant_no,
- PlDocumentReminder.del_flag == 0,
- )
- if status:
- query = query.filter(PlDocumentReminder.status == status)
- query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no)
- return query.order_by(PlDocumentReminder.expire_date.asc()).all()
- @router.put("/document-reminders/{id}", response_model=DocumentReminderRead)
- def update_document_reminder(
- id: str,
- payload: DocumentReminderUpdate,
- db: Session = Depends(get_db),
- user: DbUserContext = Depends(get_db_current_user),
- ) -> DocumentReminderRead:
- tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
- reminder = (
- db.query(PlDocumentReminder)
- .filter(
- PlDocumentReminder.id == id,
- PlDocumentReminder.tenant_no == tenant_no,
- PlDocumentReminder.del_flag == 0,
- )
- .first()
- )
- if not reminder:
- raise HTTPException(status_code=404, detail="提醒不存在")
- assert_pl_row_in_scope(db, reminder, PlDocumentReminder, user, tenant_no)
- reminder.status = payload.status
- if payload.remark:
- reminder.remark = payload.remark
- db.commit()
- db.refresh(reminder)
- return reminder
- # =============================================================================
- # 生成证件到期提醒(定时任务调用)
- # =============================================================================
- @router.post("/check-document-expiry")
- def check_document_expiry(db: Session = Depends(get_db)) -> dict:
- """检查证件到期,生成提醒(供定时任务调用)"""
- created_count = scan_cert_expiry(db)
- db.commit()
- return {"created_count": created_count}
|