material.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. """市场物料库 API"""
  2. from typing import Optional
  3. from fastapi import APIRouter, Depends, Query
  4. from pydantic import BaseModel, ConfigDict, Field
  5. from sqlalchemy.orm import Session
  6. from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload
  7. from app.core.database import get_db
  8. from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
  9. from app.core.response import error_response, success_response
  10. from app.crud import material as mat_svc
  11. from app.crud import material_stock_log as msl_svc
  12. router = APIRouter(prefix="/web/admin", tags=["市场物料"])
  13. # ---------- Body models ----------
  14. class MaterialBody(BaseModel):
  15. model_config = ConfigDict(populate_by_name=True)
  16. material_code: str = Field(..., alias="materialCode")
  17. material_name: str = Field(..., alias="materialName")
  18. category: Optional[str] = None
  19. spec_model: Optional[str] = Field(None, alias="specModel")
  20. unit: Optional[str] = None
  21. status: str = "1"
  22. order_num: Optional[int] = Field(None, alias="orderNum")
  23. remark: Optional[str] = None
  24. image_urls: Optional[str] = Field(None, alias="imageUrls")
  25. class MaterialUpdateBody(BaseModel):
  26. model_config = ConfigDict(populate_by_name=True)
  27. material_code: Optional[str] = Field(None, alias="materialCode")
  28. material_name: Optional[str] = Field(None, alias="materialName")
  29. category: Optional[str] = None
  30. spec_model: Optional[str] = Field(None, alias="specModel")
  31. unit: Optional[str] = None
  32. status: Optional[str] = None
  33. order_num: Optional[int] = Field(None, alias="orderNum")
  34. remark: Optional[str] = None
  35. image_urls: Optional[str] = Field(None, alias="imageUrls")
  36. class StockInBody(BaseModel):
  37. model_config = ConfigDict(populate_by_name=True)
  38. qty: float = Field(..., gt=0)
  39. remark: Optional[str] = None
  40. class StockOutBody(BaseModel):
  41. model_config = ConfigDict(populate_by_name=True)
  42. qty: float = Field(..., gt=0)
  43. receiver: Optional[str] = None
  44. purpose: Optional[str] = None
  45. remark: Optional[str] = None
  46. # ---------- 物料 CRUD ----------
  47. @router.get("/materials")
  48. async def list_materials(
  49. page: int = Query(1, ge=1),
  50. pageSize: int = Query(10, ge=1, le=100),
  51. keyword: Optional[str] = None,
  52. category: Optional[str] = None,
  53. status: Optional[str] = None,
  54. db: Session = Depends(get_db),
  55. user: dict = Depends(require_menu_code("biz:material:list")),
  56. ):
  57. try:
  58. tenant_no = _get_tenant_no(db, user)
  59. items, total = mat_svc.list_materials(
  60. db, tenant_no, page=page, page_size=pageSize,
  61. keyword=keyword, category=category, status=status, user_info=user,
  62. )
  63. return success_response(_page_payload(items, total, page, pageSize))
  64. except PermissionError as e:
  65. return error_response(str(e), code=403)
  66. except Exception as e:
  67. return error_response(str(e))
  68. @router.get("/materials/options")
  69. async def material_options(
  70. db: Session = Depends(get_db),
  71. user: dict = Depends(require_menu_code("biz:material:list")),
  72. ):
  73. try:
  74. tenant_no = _get_tenant_no(db, user)
  75. return success_response(mat_svc.options_list(db, tenant_no=tenant_no))
  76. except Exception as e:
  77. return error_response(str(e))
  78. @router.post("/materials")
  79. async def create_material(
  80. body: MaterialBody,
  81. db: Session = Depends(get_db),
  82. user: dict = Depends(require_menu_code("biz:material:list")),
  83. ):
  84. try:
  85. tenant_no = _get_tenant_no(db, user)
  86. user_id = _get_current_user_id(user)
  87. obj = mat_svc.create_material(
  88. db, body.model_dump(by_alias=True, exclude_none=True),
  89. tenant_no=tenant_no, user_id=user_id,
  90. )
  91. return success_response(obj, message="创建成功")
  92. except ValueError as e:
  93. return error_response(str(e))
  94. except Exception as e:
  95. return error_response(str(e))
  96. @router.put("/materials/{material_id}")
  97. async def update_material(
  98. material_id: str,
  99. body: MaterialUpdateBody,
  100. db: Session = Depends(get_db),
  101. user: dict = Depends(require_menu_code("biz:material:list")),
  102. ):
  103. try:
  104. tenant_no = _get_tenant_no(db, user)
  105. user_id = _get_current_user_id(user)
  106. obj = mat_svc.update_material(
  107. db, material_id, body.model_dump(by_alias=True, exclude_none=True),
  108. tenant_no=tenant_no, user_id=user_id,
  109. )
  110. return success_response(obj, message="更新成功")
  111. except ValueError as e:
  112. return error_response(str(e))
  113. except Exception as e:
  114. return error_response(str(e))
  115. @router.delete("/materials/{material_id}")
  116. async def delete_material(
  117. material_id: str,
  118. db: Session = Depends(get_db),
  119. user: dict = Depends(require_menu_code("biz:material:list")),
  120. ):
  121. try:
  122. tenant_no = _get_tenant_no(db, user)
  123. user_id = _get_current_user_id(user)
  124. mat_svc.soft_delete_material(db, material_id, tenant_no=tenant_no, user_id=user_id)
  125. return success_response(None, message="删除成功")
  126. except ValueError as e:
  127. return error_response(str(e))
  128. except Exception as e:
  129. return error_response(str(e))
  130. @router.post("/materials/{material_id}/stock-in")
  131. async def stock_in_material(
  132. material_id: str,
  133. body: StockInBody,
  134. db: Session = Depends(get_db),
  135. user: dict = Depends(require_menu_code("biz:material:list")),
  136. ):
  137. try:
  138. tenant_no = _get_tenant_no(db, user)
  139. user_id = _get_current_user_id(user)
  140. obj = mat_svc.stock_in(
  141. db, material_id, qty=body.qty, remark=body.remark,
  142. tenant_no=tenant_no, user_id=user_id,
  143. )
  144. return success_response(obj, message="入库成功")
  145. except ValueError as e:
  146. return error_response(str(e))
  147. except Exception as e:
  148. return error_response(str(e))
  149. @router.post("/materials/{material_id}/stock-out")
  150. async def stock_out_material(
  151. material_id: str,
  152. body: StockOutBody,
  153. db: Session = Depends(get_db),
  154. user: dict = Depends(require_menu_code("biz:material:list")),
  155. ):
  156. try:
  157. tenant_no = _get_tenant_no(db, user)
  158. user_id = _get_current_user_id(user)
  159. obj = mat_svc.stock_out(
  160. db, material_id, qty=body.qty, receiver=body.receiver,
  161. purpose=body.purpose, remark=body.remark,
  162. tenant_no=tenant_no, user_id=user_id,
  163. )
  164. return success_response(obj, message="出库成功")
  165. except ValueError as e:
  166. return error_response(str(e))
  167. except Exception as e:
  168. return error_response(str(e))
  169. # ---------- 库存变动记录 ----------
  170. @router.get("/material-stock-logs")
  171. async def list_stock_logs(
  172. page: int = Query(1, ge=1),
  173. pageSize: int = Query(10, ge=1, le=100),
  174. materialId: Optional[str] = None,
  175. changeType: Optional[str] = None,
  176. startTime: Optional[str] = None,
  177. endTime: Optional[str] = None,
  178. keyword: Optional[str] = None,
  179. db: Session = Depends(get_db),
  180. user: dict = Depends(require_menu_code("biz:materialStockLog:list")),
  181. ):
  182. try:
  183. tenant_no = _get_tenant_no(db, user)
  184. items, total = msl_svc.list_stock_logs(
  185. db, tenant_no, page=page, page_size=pageSize,
  186. material_id=materialId, change_type=changeType,
  187. start_time=startTime, end_time=endTime, keyword=keyword, user_info=user,
  188. )
  189. return success_response(_page_payload(items, total, page, pageSize))
  190. except PermissionError as e:
  191. return error_response(str(e), code=403)
  192. except Exception as e:
  193. return error_response(str(e))