inventory.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. """运输计划管理 - 盘存统计 API"""
  2. from datetime import date
  3. from decimal import Decimal
  4. from typing import Optional
  5. from fastapi import APIRouter, Depends, HTTPException, Query
  6. from sqlalchemy import func
  7. from sqlalchemy.orm import Session
  8. from app.core.database import get_db
  9. from app.core.db_auth import DbUserContext, get_db_current_user
  10. from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
  11. from app.core.tenant_context import get_tenant_no
  12. from app.core.utils import generate_uuid
  13. from app.models.pl_models import (
  14. PlDocumentReminder,
  15. PlInventory,
  16. PlInventoryTransaction,
  17. PlInventoryWarning,
  18. )
  19. from app.schemas.pl.inventory import (
  20. DocumentReminderRead,
  21. DocumentReminderStatsRead,
  22. DocumentReminderUpdate,
  23. InventoryLedgerQuery,
  24. InventoryLedgerResponse,
  25. InventoryRead,
  26. InventorySummaryItem,
  27. InventorySummaryResponse,
  28. InventoryTransactionQuery,
  29. InventoryTransactionRead,
  30. InventoryTransactionResponse,
  31. InventoryWarningRead,
  32. InventoryWarningThreshold,
  33. InventoryWarningUpdate,
  34. TransportStatsItem,
  35. TransportStatsResponse,
  36. )
  37. from app.services.pl_cert_service import scan_cert_expiry
  38. router = APIRouter(prefix="/inventory", tags=["运输计划-盘存统计"])
  39. # =============================================================================
  40. # 库存台账
  41. # =============================================================================
  42. @router.get("/ledger", response_model=InventoryLedgerResponse)
  43. def get_inventory_ledger(
  44. product_name: Optional[str] = Query(default=None, alias="productName", description="产品名称"),
  45. page: int = Query(default=1, ge=1),
  46. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  47. db: Session = Depends(get_db),
  48. user: DbUserContext = Depends(get_db_current_user),
  49. ) -> InventoryLedgerResponse:
  50. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  51. query = db.query(PlInventory).filter(
  52. PlInventory.tenant_no == tenant_no,
  53. PlInventory.del_flag == 0,
  54. )
  55. if product_name:
  56. query = query.filter(PlInventory.product_name.contains(product_name))
  57. query = apply_pl_scope(query, PlInventory, user, db, tenant_no)
  58. total = query.count()
  59. offset = (page - 1) * page_size
  60. items = query.order_by(PlInventory.product_name).offset(offset).limit(page_size).all()
  61. return InventoryLedgerResponse(total=total, list=items)
  62. # =============================================================================
  63. # 库存预警阈值设置
  64. # =============================================================================
  65. @router.post("/warning-threshold")
  66. def set_warning_threshold(
  67. payload: InventoryWarningThreshold,
  68. db: Session = Depends(get_db),
  69. user: DbUserContext = Depends(get_db_current_user),
  70. ) -> dict:
  71. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  72. inventory = (
  73. db.query(PlInventory)
  74. .filter(
  75. PlInventory.product_id == payload.product_id,
  76. PlInventory.tenant_no == tenant_no,
  77. PlInventory.del_flag == 0,
  78. )
  79. .first()
  80. )
  81. if inventory:
  82. assert_pl_row_in_scope(db, inventory, PlInventory, user, tenant_no)
  83. inventory.low_stock_warning = payload.low_stock_warning
  84. inventory.high_stock_warning = payload.high_stock_warning
  85. else:
  86. # 创建库存记录
  87. inventory = PlInventory(
  88. id=generate_uuid(),
  89. tenant_no=tenant_no,
  90. product_id=payload.product_id,
  91. product_no="",
  92. product_name="",
  93. unit="吨",
  94. stock_qty=0,
  95. stock_amount=0,
  96. low_stock_warning=payload.low_stock_warning,
  97. high_stock_warning=payload.high_stock_warning,
  98. create_by=user.id,
  99. create_dept_id=user.dept_id,
  100. )
  101. db.add(inventory)
  102. db.commit()
  103. return {"success": True}
  104. # =============================================================================
  105. # 库存预警列表
  106. # =============================================================================
  107. @router.get("/warnings", response_model=list[InventoryWarningRead])
  108. def list_warnings(
  109. status: Optional[str] = Query(default=None, description="状态"),
  110. db: Session = Depends(get_db),
  111. user: DbUserContext = Depends(get_db_current_user),
  112. ) -> list[InventoryWarningRead]:
  113. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  114. query = db.query(PlInventoryWarning).filter(
  115. PlInventoryWarning.tenant_no == tenant_no,
  116. PlInventoryWarning.del_flag == 0,
  117. )
  118. if status:
  119. query = query.filter(PlInventoryWarning.status == status)
  120. query = apply_pl_scope(query, PlInventoryWarning, user, db, tenant_no)
  121. return query.order_by(PlInventoryWarning.create_time.desc()).all()
  122. @router.put("/warnings/{id}", response_model=InventoryWarningRead)
  123. def update_warning(
  124. id: str,
  125. payload: InventoryWarningUpdate,
  126. db: Session = Depends(get_db),
  127. user: DbUserContext = Depends(get_db_current_user),
  128. ) -> InventoryWarningRead:
  129. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  130. warning = (
  131. db.query(PlInventoryWarning)
  132. .filter(
  133. PlInventoryWarning.id == id,
  134. PlInventoryWarning.tenant_no == tenant_no,
  135. PlInventoryWarning.del_flag == 0,
  136. )
  137. .first()
  138. )
  139. if not warning:
  140. raise HTTPException(status_code=404, detail="预警不存在")
  141. assert_pl_row_in_scope(db, warning, PlInventoryWarning, user, tenant_no)
  142. if payload.status == "resolved":
  143. warning.status = "resolved"
  144. from datetime import datetime
  145. warning.resolve_time = datetime.now()
  146. warning.resolve_remark = payload.resolve_remark
  147. db.commit()
  148. db.refresh(warning)
  149. return warning
  150. # =============================================================================
  151. # 出入库明细
  152. # =============================================================================
  153. @router.get("/transactions", response_model=InventoryTransactionResponse)
  154. def list_transactions(
  155. product_id: Optional[str] = Query(default=None, alias="productId", description="产品ID"),
  156. transaction_type: Optional[str] = Query(default=None, alias="transactionType", description="事务类型"),
  157. date_from: Optional[date] = Query(default=None, alias="dateFrom", description="开始日期"),
  158. date_to: Optional[date] = Query(default=None, alias="dateTo", description="结束日期"),
  159. page: int = Query(default=1, ge=1),
  160. page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
  161. db: Session = Depends(get_db),
  162. user: DbUserContext = Depends(get_db_current_user),
  163. ) -> InventoryTransactionResponse:
  164. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  165. query = db.query(PlInventoryTransaction).filter(
  166. PlInventoryTransaction.tenant_no == tenant_no,
  167. PlInventoryTransaction.del_flag == 0,
  168. )
  169. if product_id:
  170. query = query.filter(PlInventoryTransaction.product_id == product_id)
  171. if transaction_type:
  172. query = query.filter(PlInventoryTransaction.transaction_type == transaction_type)
  173. if date_from:
  174. query = query.filter(PlInventoryTransaction.create_time >= date_from)
  175. if date_to:
  176. query = query.filter(PlInventoryTransaction.create_time <= date_to)
  177. query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)
  178. total = query.count()
  179. offset = (page - 1) * page_size
  180. items = query.order_by(PlInventoryTransaction.create_time.desc()).offset(offset).limit(page_size).all()
  181. return InventoryTransactionResponse(total=total, list=items)
  182. # =============================================================================
  183. # 出入库汇总
  184. # =============================================================================
  185. @router.get("/summary", response_model=InventorySummaryResponse)
  186. def get_summary(
  187. group_by: str = Query(default="month", description="汇总维度 month/week"),
  188. date_from: date = Query(..., alias="dateFrom", description="开始日期"),
  189. date_to: date = Query(..., alias="dateTo", description="结束日期"),
  190. product_id: Optional[str] = Query(default=None, alias="productId", description="产品ID"),
  191. db: Session = Depends(get_db),
  192. user: DbUserContext = Depends(get_db_current_user),
  193. ) -> InventorySummaryResponse:
  194. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  195. query = db.query(PlInventoryTransaction).filter(
  196. PlInventoryTransaction.tenant_no == tenant_no,
  197. PlInventoryTransaction.del_flag == 0,
  198. PlInventoryTransaction.create_time >= date_from,
  199. PlInventoryTransaction.create_time <= date_to,
  200. )
  201. query = apply_pl_scope(query, PlInventoryTransaction, user, db, tenant_no)
  202. if product_id:
  203. query = query.filter(PlInventoryTransaction.product_id == product_id)
  204. transactions = query.all()
  205. # 按产品分组汇总
  206. summary_map = {}
  207. for t in transactions:
  208. key = t.product_id
  209. if key not in summary_map:
  210. summary_map[key] = {
  211. "product_id": t.product_id,
  212. "product_name": t.product_name,
  213. "inbound_qty": Decimal("0"),
  214. "outbound_qty": Decimal("0"),
  215. }
  216. if t.transaction_type == "inbound":
  217. summary_map[key]["inbound_qty"] += t.qty
  218. else:
  219. summary_map[key]["outbound_qty"] += abs(t.qty)
  220. items = []
  221. for key, data in summary_map.items():
  222. net = data["inbound_qty"] - data["outbound_qty"]
  223. items.append(InventorySummaryItem(
  224. period="汇总",
  225. product_id=data["product_id"],
  226. product_name=data["product_name"],
  227. inbound_qty=data["inbound_qty"],
  228. outbound_qty=data["outbound_qty"],
  229. net_change=net,
  230. ))
  231. return InventorySummaryResponse(list=items)
  232. # =============================================================================
  233. # 证件到期提醒
  234. # =============================================================================
  235. @router.get("/document-reminders/stats", response_model=DocumentReminderStatsRead)
  236. def document_reminder_stats(
  237. db: Session = Depends(get_db),
  238. user: DbUserContext = Depends(get_db_current_user),
  239. ) -> DocumentReminderStatsRead:
  240. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  241. scan_cert_expiry(db, tenant_no)
  242. db.commit()
  243. query = db.query(PlDocumentReminder).filter(
  244. PlDocumentReminder.tenant_no == tenant_no,
  245. PlDocumentReminder.del_flag == 0,
  246. PlDocumentReminder.status == "pending",
  247. PlDocumentReminder.days_remaining <= 0,
  248. )
  249. query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no)
  250. expired_pending_count = query.count()
  251. return DocumentReminderStatsRead(expired_pending_count=expired_pending_count)
  252. @router.get("/document-reminders", response_model=list[DocumentReminderRead])
  253. def list_document_reminders(
  254. status: Optional[str] = Query(default=None, description="状态"),
  255. db: Session = Depends(get_db),
  256. user: DbUserContext = Depends(get_db_current_user),
  257. ) -> list[DocumentReminderRead]:
  258. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  259. scan_cert_expiry(db, tenant_no)
  260. db.commit()
  261. query = db.query(PlDocumentReminder).filter(
  262. PlDocumentReminder.tenant_no == tenant_no,
  263. PlDocumentReminder.del_flag == 0,
  264. )
  265. if status:
  266. query = query.filter(PlDocumentReminder.status == status)
  267. query = apply_pl_scope(query, PlDocumentReminder, user, db, tenant_no)
  268. return query.order_by(PlDocumentReminder.expire_date.asc()).all()
  269. @router.put("/document-reminders/{id}", response_model=DocumentReminderRead)
  270. def update_document_reminder(
  271. id: str,
  272. payload: DocumentReminderUpdate,
  273. db: Session = Depends(get_db),
  274. user: DbUserContext = Depends(get_db_current_user),
  275. ) -> DocumentReminderRead:
  276. tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
  277. reminder = (
  278. db.query(PlDocumentReminder)
  279. .filter(
  280. PlDocumentReminder.id == id,
  281. PlDocumentReminder.tenant_no == tenant_no,
  282. PlDocumentReminder.del_flag == 0,
  283. )
  284. .first()
  285. )
  286. if not reminder:
  287. raise HTTPException(status_code=404, detail="提醒不存在")
  288. assert_pl_row_in_scope(db, reminder, PlDocumentReminder, user, tenant_no)
  289. reminder.status = payload.status
  290. if payload.remark:
  291. reminder.remark = payload.remark
  292. db.commit()
  293. db.refresh(reminder)
  294. return reminder
  295. # =============================================================================
  296. # 生成证件到期提醒(定时任务调用)
  297. # =============================================================================
  298. @router.post("/check-document-expiry")
  299. def check_document_expiry(db: Session = Depends(get_db)) -> dict:
  300. """检查证件到期,生成提醒(供定时任务调用)"""
  301. created_count = scan_cert_expiry(db)
  302. db.commit()
  303. return {"created_count": created_count}