production.py 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057
  1. """生产管理 API 路由(7 个子模块:耗材、试剂、SOP文档、工艺工序分类、产出物类型、耗材包、工艺库)"""
  2. from fastapi import APIRouter, Depends, Query
  3. from pydantic import BaseModel, ConfigDict, Field
  4. from sqlalchemy.orm import Session
  5. from typing import Any, Optional, List
  6. from app.core.database import get_db
  7. from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
  8. from app.core.response import success_response, error_response
  9. from app.crud import consumable as consumable_svc
  10. from app.crud import reagent as reagent_svc
  11. from app.crud import consumable_stock_log as consumable_stock_log_svc
  12. from app.crud import reagent_stock_log as reagent_stock_log_svc
  13. from app.crud import sop_document as sop_document_svc
  14. from app.crud import process_category as process_category_svc
  15. from app.crud import output_type as output_type_svc
  16. from app.crud import consumable_pack as consumable_pack_svc
  17. from app.crud import process as process_svc
  18. from app.crud import process_route as route_svc
  19. from app.schemas.process_route import (
  20. ProcessRouteCreateBody,
  21. ProcessRouteUpdateBody,
  22. SetCurrentBody,
  23. StepAddBody,
  24. StepCopyBody,
  25. StepUpdateBody,
  26. StepReorderBody,
  27. StepReagentUpdateBody,
  28. StepConsumableUpdateBody,
  29. StepOutputUpdateBody,
  30. StepDataCollectionUpdateBody,
  31. )
  32. from app.models.models import SysUser
  33. from app.crud import production_work_order as production_work_order_svc
  34. from app.crud import production_task as production_task_svc
  35. from app.crud import cell_product as cell_product_svc
  36. from app.schemas.production_work_order import ProductionWorkOrderListQuery, ProductionWorkOrderComplete
  37. from app.schemas.production_task import (
  38. ProductionTaskCreate,
  39. ProductionTaskUpdate,
  40. ProductionTaskStatusUpdate,
  41. TaskStepAdd,
  42. TaskStepUpdate,
  43. TaskStepMaterialItem,
  44. TaskStepOutputItem,
  45. TaskStepDataCollectionItem,
  46. TaskInputItem,
  47. TaskOutputItem,
  48. TaskStepReorderBody,
  49. )
  50. from app.schemas.cell_product import CellProductCreate, CellProductUpdate
  51. router = APIRouter(prefix="/web/admin", tags=["生产管理"])
  52. def _get_tenant_no(db: Session, user: dict) -> str:
  53. user_id = user.get("user_id")
  54. if not user_id:
  55. return "zkxw"
  56. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  57. return (sys_user.tenant_no if sys_user else None) or "zkxw"
  58. # ---------------------------------------------------------------------------
  59. # 1. 耗材
  60. # ---------------------------------------------------------------------------
  61. class ConsumableCreateBody(BaseModel):
  62. model_config = ConfigDict(populate_by_name=True)
  63. consumable_code: str = Field(..., alias="consumableCode")
  64. consumable_name: str = Field(..., alias="consumableName")
  65. spec_model: Optional[str] = Field(None, alias="specModel")
  66. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  67. unit: Optional[str] = None
  68. description: Optional[str] = None
  69. warning_qty: Optional[float] = Field(0, alias="warningQty")
  70. status: Optional[str] = Field("1", alias="status")
  71. order_num: Optional[int] = Field(0, alias="orderNum")
  72. remark: Optional[str] = None
  73. class ConsumableUpdateBody(BaseModel):
  74. model_config = ConfigDict(populate_by_name=True)
  75. consumable_code: Optional[str] = Field(None, alias="consumableCode")
  76. consumable_name: Optional[str] = Field(None, alias="consumableName")
  77. spec_model: Optional[str] = Field(None, alias="specModel")
  78. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  79. unit: Optional[str] = None
  80. description: Optional[str] = None
  81. warning_qty: Optional[float] = Field(None, alias="warningQty")
  82. status: Optional[str] = Field(None, alias="status")
  83. order_num: Optional[int] = Field(None, alias="orderNum")
  84. remark: Optional[str] = None
  85. @router.get("/consumables")
  86. async def list_consumables(
  87. page: int = Query(1, ge=1),
  88. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  89. keyword: Optional[str] = None,
  90. user: dict = Depends(require_menu_code("production:consumable:list")),
  91. db: Session = Depends(get_db),
  92. ):
  93. tenant_no = _get_tenant_no(db, user)
  94. items, total = consumable_svc.list_consumables(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  95. tp = consumable_svc.total_pages(total, pageSize)
  96. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  97. @router.get("/consumables/all")
  98. async def list_all_consumables(
  99. user: dict = Depends(require_admin_or_super_admin),
  100. db: Session = Depends(get_db),
  101. ):
  102. tenant_no = _get_tenant_no(db, user)
  103. items, _ = consumable_svc.list_consumables(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  104. return success_response(items)
  105. @router.post("/consumables")
  106. async def create_consumable(
  107. body: ConsumableCreateBody,
  108. user: dict = Depends(require_admin_or_super_admin),
  109. db: Session = Depends(get_db),
  110. ):
  111. try:
  112. tenant_no = _get_tenant_no(db, user)
  113. obj = consumable_svc.create_consumable(
  114. db,
  115. consumable_code=body.consumable_code,
  116. consumable_name=body.consumable_name,
  117. spec_model=body.spec_model,
  118. standard_usage=body.standard_usage,
  119. unit=body.unit,
  120. description=body.description,
  121. tenant_no=tenant_no,
  122. warning_qty=body.warning_qty,
  123. status=body.status,
  124. order_num=body.order_num,
  125. remark=body.remark,
  126. user_id=user.get("user_id"),
  127. )
  128. return success_response(consumable_svc._row_to_dict(obj), message="创建成功")
  129. except ValueError as e:
  130. return error_response(400, str(e))
  131. @router.put("/consumables/{consumable_id}")
  132. async def update_consumable(
  133. consumable_id: str,
  134. body: ConsumableUpdateBody,
  135. user: dict = Depends(require_admin_or_super_admin),
  136. db: Session = Depends(get_db),
  137. ):
  138. try:
  139. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  140. obj = consumable_svc.update_consumable(
  141. db,
  142. consumable_id,
  143. consumable_code=payload.get("consumable_code"),
  144. consumable_name=payload.get("consumable_name"),
  145. spec_model=payload.get("spec_model"),
  146. standard_usage=payload.get("standard_usage"),
  147. unit=payload.get("unit"),
  148. description=payload.get("description"),
  149. warning_qty=payload.get("warning_qty"),
  150. status=payload.get("status"),
  151. order_num=payload.get("order_num"),
  152. remark=payload.get("remark"),
  153. user_id=user.get("user_id"),
  154. )
  155. return success_response(consumable_svc._row_to_dict(obj), message="更新成功")
  156. except ValueError as e:
  157. return error_response(400, str(e))
  158. @router.delete("/consumables/{consumable_id}")
  159. async def delete_consumable(
  160. consumable_id: str,
  161. _user: dict = Depends(require_admin_or_super_admin),
  162. db: Session = Depends(get_db),
  163. ):
  164. try:
  165. consumable_svc.soft_delete_consumable(db, consumable_id)
  166. return success_response(None, message="删除成功")
  167. except ValueError as e:
  168. return error_response(400, str(e))
  169. class StockInBody(BaseModel):
  170. model_config = ConfigDict(populate_by_name=True)
  171. qty: float = Field(..., gt=0)
  172. remark: Optional[str] = None
  173. class StockOutBody(BaseModel):
  174. model_config = ConfigDict(populate_by_name=True)
  175. qty: float = Field(..., gt=0)
  176. receiver: str = Field(..., min_length=1)
  177. purpose: Optional[str] = None
  178. remark: Optional[str] = None
  179. class BatchStockInItem(BaseModel):
  180. model_config = ConfigDict(populate_by_name=True)
  181. materialId: str
  182. qty: float = Field(..., gt=0)
  183. remark: Optional[str] = None
  184. class BatchStockInBody(BaseModel):
  185. model_config = ConfigDict(populate_by_name=True)
  186. items: List[BatchStockInItem]
  187. remark: Optional[str] = None
  188. class BatchStockOutItem(BaseModel):
  189. model_config = ConfigDict(populate_by_name=True)
  190. materialId: str
  191. qty: float = Field(..., gt=0)
  192. class BatchStockOutBody(BaseModel):
  193. model_config = ConfigDict(populate_by_name=True)
  194. items: List[BatchStockOutItem]
  195. receiver: str = Field(..., min_length=1)
  196. purpose: Optional[str] = None
  197. remark: Optional[str] = None
  198. @router.post("/consumables/{consumable_id}/stock-in")
  199. async def consumable_stock_in(
  200. consumable_id: str,
  201. body: StockInBody,
  202. user: dict = Depends(require_admin_or_super_admin),
  203. db: Session = Depends(get_db),
  204. ):
  205. try:
  206. tenant_no = _get_tenant_no(db, user)
  207. from decimal import Decimal
  208. obj = consumable_svc.stock_in(
  209. db, consumable_id,
  210. qty=Decimal(str(body.qty)),
  211. remark=body.remark,
  212. user_id=user.get("user_id"),
  213. tenant_no=tenant_no,
  214. )
  215. return success_response(consumable_svc._row_to_dict(obj), message="入库成功")
  216. except ValueError as e:
  217. return error_response(400, str(e))
  218. @router.post("/consumables/{consumable_id}/stock-out")
  219. async def consumable_stock_out(
  220. consumable_id: str,
  221. body: StockOutBody,
  222. user: dict = Depends(require_admin_or_super_admin),
  223. db: Session = Depends(get_db),
  224. ):
  225. try:
  226. tenant_no = _get_tenant_no(db, user)
  227. from decimal import Decimal
  228. obj = consumable_svc.stock_out(
  229. db, consumable_id,
  230. qty=Decimal(str(body.qty)),
  231. receiver=body.receiver,
  232. purpose=body.purpose,
  233. remark=body.remark,
  234. user_id=user.get("user_id"),
  235. tenant_no=tenant_no,
  236. )
  237. return success_response(consumable_svc._row_to_dict(obj), message="出库成功")
  238. except ValueError as e:
  239. return error_response(400, str(e))
  240. @router.post("/consumables/batch-stock-in")
  241. async def consumable_batch_stock_in(
  242. body: BatchStockInBody,
  243. user: dict = Depends(require_admin_or_super_admin),
  244. db: Session = Depends(get_db),
  245. ):
  246. try:
  247. tenant_no = _get_tenant_no(db, user)
  248. items_data = [it.model_dump() for it in body.items]
  249. count = consumable_svc.batch_stock_in(
  250. db, items=items_data, remark=body.remark,
  251. tenant_no=tenant_no, user_id=user.get("user_id"),
  252. )
  253. return success_response({"count": count}, message=f"成功入库 {count} 条")
  254. except ValueError as e:
  255. return error_response(400, str(e))
  256. @router.post("/consumables/batch-stock-out")
  257. async def consumable_batch_stock_out(
  258. body: BatchStockOutBody,
  259. user: dict = Depends(require_admin_or_super_admin),
  260. db: Session = Depends(get_db),
  261. ):
  262. try:
  263. tenant_no = _get_tenant_no(db, user)
  264. items_data = [it.model_dump() for it in body.items]
  265. count = consumable_svc.batch_stock_out(
  266. db, items=items_data,
  267. receiver=body.receiver, purpose=body.purpose, remark=body.remark,
  268. tenant_no=tenant_no, user_id=user.get("user_id"),
  269. )
  270. return success_response({"count": count}, message=f"成功出库 {count} 条")
  271. except ValueError as e:
  272. return error_response(400, str(e))
  273. @router.get("/consumables/{consumable_id}/stock-logs")
  274. async def list_consumable_stock_logs(
  275. consumable_id: str,
  276. page: int = Query(1, ge=1),
  277. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  278. changeType: Optional[str] = None,
  279. user: dict = Depends(require_admin_or_super_admin),
  280. db: Session = Depends(get_db),
  281. ):
  282. tenant_no = _get_tenant_no(db, user)
  283. items, total = consumable_stock_log_svc.list_stock_logs(
  284. db, tenant_no, page=page, page_size=pageSize,
  285. material_id=consumable_id, change_type=changeType,
  286. user_info=user,
  287. )
  288. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  289. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  290. @router.get("/consumables/stock-logs")
  291. async def list_all_consumable_stock_logs(
  292. page: int = Query(1, ge=1),
  293. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  294. keyword: Optional[str] = None,
  295. changeType: Optional[str] = None,
  296. startTime: Optional[str] = None,
  297. endTime: Optional[str] = None,
  298. user: dict = Depends(require_admin_or_super_admin),
  299. db: Session = Depends(get_db),
  300. ):
  301. tenant_no = _get_tenant_no(db, user)
  302. items, total = consumable_stock_log_svc.list_stock_logs(
  303. db, tenant_no, page=page, page_size=pageSize,
  304. keyword=keyword, change_type=changeType,
  305. start_time=startTime, end_time=endTime,
  306. user_info=user,
  307. )
  308. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  309. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  310. # ---------------------------------------------------------------------------
  311. # 2. 试剂
  312. # ---------------------------------------------------------------------------
  313. class ReagentCreateBody(BaseModel):
  314. model_config = ConfigDict(populate_by_name=True)
  315. reagent_code: str = Field(..., alias="reagentCode")
  316. reagent_name: str = Field(..., alias="reagentName")
  317. spec: Optional[str] = None
  318. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  319. unit: Optional[str] = None
  320. description: Optional[str] = None
  321. warning_qty: Optional[float] = Field(0, alias="warningQty")
  322. status: Optional[str] = Field("1", alias="status")
  323. order_num: Optional[int] = Field(0, alias="orderNum")
  324. remark: Optional[str] = None
  325. class ReagentUpdateBody(BaseModel):
  326. model_config = ConfigDict(populate_by_name=True)
  327. reagent_code: Optional[str] = Field(None, alias="reagentCode")
  328. reagent_name: Optional[str] = Field(None, alias="reagentName")
  329. spec: Optional[str] = None
  330. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  331. unit: Optional[str] = None
  332. description: Optional[str] = None
  333. warning_qty: Optional[float] = Field(None, alias="warningQty")
  334. status: Optional[str] = Field(None, alias="status")
  335. order_num: Optional[int] = Field(None, alias="orderNum")
  336. remark: Optional[str] = None
  337. @router.get("/reagents")
  338. async def list_reagents(
  339. page: int = Query(1, ge=1),
  340. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  341. keyword: Optional[str] = None,
  342. user: dict = Depends(require_menu_code("production:reagent:list")),
  343. db: Session = Depends(get_db),
  344. ):
  345. tenant_no = _get_tenant_no(db, user)
  346. items, total = reagent_svc.list_reagents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  347. tp = reagent_svc.total_pages(total, pageSize)
  348. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  349. @router.get("/reagents/all")
  350. async def list_all_reagents(
  351. user: dict = Depends(require_admin_or_super_admin),
  352. db: Session = Depends(get_db),
  353. ):
  354. tenant_no = _get_tenant_no(db, user)
  355. items, _ = reagent_svc.list_reagents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  356. return success_response(items)
  357. @router.post("/reagents")
  358. async def create_reagent(
  359. body: ReagentCreateBody,
  360. user: dict = Depends(require_admin_or_super_admin),
  361. db: Session = Depends(get_db),
  362. ):
  363. try:
  364. tenant_no = _get_tenant_no(db, user)
  365. obj = reagent_svc.create_reagent(
  366. db,
  367. reagent_code=body.reagent_code,
  368. reagent_name=body.reagent_name,
  369. spec=body.spec,
  370. standard_usage=body.standard_usage,
  371. unit=body.unit,
  372. description=body.description,
  373. tenant_no=tenant_no,
  374. warning_qty=body.warning_qty,
  375. status=body.status,
  376. order_num=body.order_num,
  377. remark=body.remark,
  378. user_id=user.get("user_id"),
  379. )
  380. return success_response(reagent_svc._row_to_dict(obj), message="创建成功")
  381. except ValueError as e:
  382. return error_response(400, str(e))
  383. @router.put("/reagents/{reagent_id}")
  384. async def update_reagent(
  385. reagent_id: str,
  386. body: ReagentUpdateBody,
  387. user: dict = Depends(require_admin_or_super_admin),
  388. db: Session = Depends(get_db),
  389. ):
  390. try:
  391. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  392. obj = reagent_svc.update_reagent(
  393. db,
  394. reagent_id,
  395. reagent_code=payload.get("reagent_code"),
  396. reagent_name=payload.get("reagent_name"),
  397. spec=payload.get("spec"),
  398. standard_usage=payload.get("standard_usage"),
  399. unit=payload.get("unit"),
  400. description=payload.get("description"),
  401. warning_qty=payload.get("warning_qty"),
  402. status=payload.get("status"),
  403. order_num=payload.get("order_num"),
  404. remark=payload.get("remark"),
  405. user_id=user.get("user_id"),
  406. )
  407. return success_response(reagent_svc._row_to_dict(obj), message="更新成功")
  408. except ValueError as e:
  409. return error_response(400, str(e))
  410. @router.delete("/reagents/{reagent_id}")
  411. async def delete_reagent(
  412. reagent_id: str,
  413. _user: dict = Depends(require_admin_or_super_admin),
  414. db: Session = Depends(get_db),
  415. ):
  416. try:
  417. reagent_svc.soft_delete_reagent(db, reagent_id)
  418. return success_response(None, message="删除成功")
  419. except ValueError as e:
  420. return error_response(400, str(e))
  421. @router.post("/reagents/{reagent_id}/stock-in")
  422. async def reagent_stock_in(
  423. reagent_id: str,
  424. body: StockInBody,
  425. user: dict = Depends(require_admin_or_super_admin),
  426. db: Session = Depends(get_db),
  427. ):
  428. try:
  429. tenant_no = _get_tenant_no(db, user)
  430. from decimal import Decimal
  431. obj = reagent_svc.stock_in(
  432. db, reagent_id,
  433. qty=Decimal(str(body.qty)),
  434. remark=body.remark,
  435. user_id=user.get("user_id"),
  436. tenant_no=tenant_no,
  437. )
  438. return success_response(reagent_svc._row_to_dict(obj), message="入库成功")
  439. except ValueError as e:
  440. return error_response(400, str(e))
  441. @router.post("/reagents/{reagent_id}/stock-out")
  442. async def reagent_stock_out(
  443. reagent_id: str,
  444. body: StockOutBody,
  445. user: dict = Depends(require_admin_or_super_admin),
  446. db: Session = Depends(get_db),
  447. ):
  448. try:
  449. tenant_no = _get_tenant_no(db, user)
  450. from decimal import Decimal
  451. obj = reagent_svc.stock_out(
  452. db, reagent_id,
  453. qty=Decimal(str(body.qty)),
  454. receiver=body.receiver,
  455. purpose=body.purpose,
  456. remark=body.remark,
  457. user_id=user.get("user_id"),
  458. tenant_no=tenant_no,
  459. )
  460. return success_response(reagent_svc._row_to_dict(obj), message="出库成功")
  461. except ValueError as e:
  462. return error_response(400, str(e))
  463. @router.post("/reagents/batch-stock-in")
  464. async def reagent_batch_stock_in(
  465. body: BatchStockInBody,
  466. user: dict = Depends(require_admin_or_super_admin),
  467. db: Session = Depends(get_db),
  468. ):
  469. try:
  470. tenant_no = _get_tenant_no(db, user)
  471. items_data = [it.model_dump() for it in body.items]
  472. count = reagent_svc.batch_stock_in(
  473. db, items=items_data, remark=body.remark,
  474. tenant_no=tenant_no, user_id=user.get("user_id"),
  475. )
  476. return success_response({"count": count}, message=f"成功入库 {count} 条")
  477. except ValueError as e:
  478. return error_response(400, str(e))
  479. @router.post("/reagents/batch-stock-out")
  480. async def reagent_batch_stock_out(
  481. body: BatchStockOutBody,
  482. user: dict = Depends(require_admin_or_super_admin),
  483. db: Session = Depends(get_db),
  484. ):
  485. try:
  486. tenant_no = _get_tenant_no(db, user)
  487. items_data = [it.model_dump() for it in body.items]
  488. count = reagent_svc.batch_stock_out(
  489. db, items=items_data,
  490. receiver=body.receiver, purpose=body.purpose, remark=body.remark,
  491. tenant_no=tenant_no, user_id=user.get("user_id"),
  492. )
  493. return success_response({"count": count}, message=f"成功出库 {count} 条")
  494. except ValueError as e:
  495. return error_response(400, str(e))
  496. @router.get("/reagents/{reagent_id}/stock-logs")
  497. async def list_reagent_stock_logs(
  498. reagent_id: str,
  499. page: int = Query(1, ge=1),
  500. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  501. changeType: Optional[str] = None,
  502. user: dict = Depends(require_admin_or_super_admin),
  503. db: Session = Depends(get_db),
  504. ):
  505. tenant_no = _get_tenant_no(db, user)
  506. items, total = reagent_stock_log_svc.list_stock_logs(
  507. db, tenant_no, page=page, page_size=pageSize,
  508. material_id=reagent_id, change_type=changeType,
  509. user_info=user,
  510. )
  511. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  512. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  513. @router.get("/reagents/stock-logs")
  514. async def list_all_reagent_stock_logs(
  515. page: int = Query(1, ge=1),
  516. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  517. keyword: Optional[str] = None,
  518. changeType: Optional[str] = None,
  519. startTime: Optional[str] = None,
  520. endTime: Optional[str] = None,
  521. user: dict = Depends(require_admin_or_super_admin),
  522. db: Session = Depends(get_db),
  523. ):
  524. tenant_no = _get_tenant_no(db, user)
  525. items, total = reagent_stock_log_svc.list_stock_logs(
  526. db, tenant_no, page=page, page_size=pageSize,
  527. keyword=keyword, change_type=changeType,
  528. start_time=startTime, end_time=endTime,
  529. user_info=user,
  530. )
  531. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  532. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  533. # ---------------------------------------------------------------------------
  534. # 3. SOP文档
  535. # ---------------------------------------------------------------------------
  536. class SopDocumentCreateBody(BaseModel):
  537. model_config = ConfigDict(populate_by_name=True)
  538. sop_code: str = Field(..., alias="sopCode")
  539. sop_name: str = Field(..., alias="sopName")
  540. version_no: Optional[str] = Field(None, alias="versionNo")
  541. sop_attachments: Optional[str] = Field(None, alias="sopAttachments")
  542. order_num: Optional[int] = Field(0, alias="orderNum")
  543. remark: Optional[str] = None
  544. class SopDocumentUpdateBody(BaseModel):
  545. model_config = ConfigDict(populate_by_name=True)
  546. sop_code: Optional[str] = Field(None, alias="sopCode")
  547. sop_name: Optional[str] = Field(None, alias="sopName")
  548. version_no: Optional[str] = Field(None, alias="versionNo")
  549. sop_attachments: Optional[str] = Field(None, alias="sopAttachments")
  550. order_num: Optional[int] = Field(None, alias="orderNum")
  551. remark: Optional[str] = None
  552. @router.get("/sop-documents")
  553. async def list_sop_documents(
  554. page: int = Query(1, ge=1),
  555. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  556. keyword: Optional[str] = None,
  557. user: dict = Depends(require_menu_code("production:sop-document:list")),
  558. db: Session = Depends(get_db),
  559. ):
  560. tenant_no = _get_tenant_no(db, user)
  561. items, total = sop_document_svc.list_sop_documents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  562. tp = sop_document_svc.total_pages(total, pageSize)
  563. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  564. @router.get("/sop-documents/all")
  565. async def list_all_sop_documents(
  566. user: dict = Depends(require_admin_or_super_admin),
  567. db: Session = Depends(get_db),
  568. ):
  569. tenant_no = _get_tenant_no(db, user)
  570. items, _ = sop_document_svc.list_sop_documents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  571. return success_response(items)
  572. @router.post("/sop-documents")
  573. async def create_sop_document(
  574. body: SopDocumentCreateBody,
  575. user: dict = Depends(require_admin_or_super_admin),
  576. db: Session = Depends(get_db),
  577. ):
  578. try:
  579. tenant_no = _get_tenant_no(db, user)
  580. obj = sop_document_svc.create_sop_document(
  581. db,
  582. sop_code=body.sop_code,
  583. sop_name=body.sop_name,
  584. version_no=body.version_no,
  585. sop_attachments=body.sop_attachments,
  586. tenant_no=tenant_no,
  587. order_num=body.order_num,
  588. remark=body.remark,
  589. user_id=user.get("user_id"),
  590. )
  591. return success_response(
  592. sop_document_svc._row_to_dict(obj, db, tenant_no),
  593. message="创建成功",
  594. )
  595. except ValueError as e:
  596. return error_response(400, str(e))
  597. @router.put("/sop-documents/{sop_id}")
  598. async def update_sop_document(
  599. sop_id: str,
  600. body: SopDocumentUpdateBody,
  601. user: dict = Depends(require_admin_or_super_admin),
  602. db: Session = Depends(get_db),
  603. ):
  604. try:
  605. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  606. obj = sop_document_svc.update_sop_document(
  607. db,
  608. sop_id,
  609. sop_code=payload.get("sop_code"),
  610. sop_name=payload.get("sop_name"),
  611. version_no=payload.get("version_no"),
  612. sop_attachments=payload.get("sop_attachments"),
  613. order_num=payload.get("order_num"),
  614. remark=payload.get("remark"),
  615. user_id=user.get("user_id"),
  616. )
  617. tenant_no = _get_tenant_no(db, user)
  618. return success_response(
  619. sop_document_svc._row_to_dict(obj, db, tenant_no),
  620. message="更新成功",
  621. )
  622. except ValueError as e:
  623. return error_response(400, str(e))
  624. @router.delete("/sop-documents/{sop_id}")
  625. async def delete_sop_document(
  626. sop_id: str,
  627. _user: dict = Depends(require_admin_or_super_admin),
  628. db: Session = Depends(get_db),
  629. ):
  630. try:
  631. sop_document_svc.soft_delete_sop_document(db, sop_id)
  632. return success_response(None, message="删除成功")
  633. except ValueError as e:
  634. return error_response(400, str(e))
  635. # ---------------------------------------------------------------------------
  636. # 4. 工艺工序分类
  637. # ---------------------------------------------------------------------------
  638. class ProcessCategoryCreateBody(BaseModel):
  639. model_config = ConfigDict(populate_by_name=True)
  640. category_code: str = Field(..., alias="categoryCode")
  641. category_name: str = Field(..., alias="categoryName")
  642. order_num: Optional[int] = Field(0, alias="orderNum")
  643. remark: Optional[str] = None
  644. class ProcessCategoryUpdateBody(BaseModel):
  645. model_config = ConfigDict(populate_by_name=True)
  646. category_code: Optional[str] = Field(None, alias="categoryCode")
  647. category_name: Optional[str] = Field(None, alias="categoryName")
  648. order_num: Optional[int] = Field(None, alias="orderNum")
  649. remark: Optional[str] = None
  650. @router.get("/process-categories")
  651. async def list_process_categories(
  652. page: int = Query(1, ge=1),
  653. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  654. keyword: Optional[str] = None,
  655. user: dict = Depends(require_menu_code("production:process-category:list")),
  656. db: Session = Depends(get_db),
  657. ):
  658. tenant_no = _get_tenant_no(db, user)
  659. items, total = process_category_svc.list_process_categories(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  660. tp = process_category_svc.total_pages(total, pageSize)
  661. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  662. @router.get("/process-categories/all")
  663. async def list_all_process_categories(
  664. user: dict = Depends(require_admin_or_super_admin),
  665. db: Session = Depends(get_db),
  666. ):
  667. tenant_no = _get_tenant_no(db, user)
  668. items, _ = process_category_svc.list_process_categories(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  669. return success_response(items)
  670. @router.post("/process-categories")
  671. async def create_process_category(
  672. body: ProcessCategoryCreateBody,
  673. user: dict = Depends(require_admin_or_super_admin),
  674. db: Session = Depends(get_db),
  675. ):
  676. try:
  677. tenant_no = _get_tenant_no(db, user)
  678. obj = process_category_svc.create_process_category(
  679. db,
  680. category_code=body.category_code,
  681. category_name=body.category_name,
  682. tenant_no=tenant_no,
  683. order_num=body.order_num,
  684. remark=body.remark,
  685. user_id=user.get("user_id"),
  686. )
  687. return success_response(process_category_svc._row_to_dict(obj), message="创建成功")
  688. except ValueError as e:
  689. return error_response(400, str(e))
  690. @router.put("/process-categories/{category_id}")
  691. async def update_process_category(
  692. category_id: str,
  693. body: ProcessCategoryUpdateBody,
  694. user: dict = Depends(require_admin_or_super_admin),
  695. db: Session = Depends(get_db),
  696. ):
  697. try:
  698. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  699. obj = process_category_svc.update_process_category(
  700. db,
  701. category_id,
  702. category_code=payload.get("category_code"),
  703. category_name=payload.get("category_name"),
  704. order_num=payload.get("order_num"),
  705. remark=payload.get("remark"),
  706. user_id=user.get("user_id"),
  707. )
  708. return success_response(process_category_svc._row_to_dict(obj), message="更新成功")
  709. except ValueError as e:
  710. return error_response(400, str(e))
  711. @router.delete("/process-categories/{category_id}")
  712. async def delete_process_category(
  713. category_id: str,
  714. _user: dict = Depends(require_admin_or_super_admin),
  715. db: Session = Depends(get_db),
  716. ):
  717. try:
  718. process_category_svc.soft_delete_process_category(db, category_id)
  719. return success_response(None, message="删除成功")
  720. except ValueError as e:
  721. return error_response(400, str(e))
  722. # ---------------------------------------------------------------------------
  723. # 5. 工艺产出物类型
  724. # ---------------------------------------------------------------------------
  725. class OutputTypeCreateBody(BaseModel):
  726. model_config = ConfigDict(populate_by_name=True)
  727. type_code: str = Field(..., alias="typeCode")
  728. type_name: str = Field(..., alias="typeName")
  729. order_num: Optional[int] = Field(0, alias="orderNum")
  730. remark: Optional[str] = None
  731. class OutputTypeUpdateBody(BaseModel):
  732. model_config = ConfigDict(populate_by_name=True)
  733. type_code: Optional[str] = Field(None, alias="typeCode")
  734. type_name: Optional[str] = Field(None, alias="typeName")
  735. order_num: Optional[int] = Field(None, alias="orderNum")
  736. remark: Optional[str] = None
  737. @router.get("/output-types")
  738. async def list_output_types(
  739. page: int = Query(1, ge=1),
  740. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  741. keyword: Optional[str] = None,
  742. user: dict = Depends(require_menu_code("production:output-type:list")),
  743. db: Session = Depends(get_db),
  744. ):
  745. tenant_no = _get_tenant_no(db, user)
  746. items, total = output_type_svc.list_output_types(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  747. tp = output_type_svc.total_pages(total, pageSize)
  748. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  749. @router.get("/output-types/all")
  750. async def list_all_output_types(
  751. user: dict = Depends(require_admin_or_super_admin),
  752. db: Session = Depends(get_db),
  753. ):
  754. tenant_no = _get_tenant_no(db, user)
  755. items, _ = output_type_svc.list_output_types(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  756. return success_response(items)
  757. @router.post("/output-types")
  758. async def create_output_type(
  759. body: OutputTypeCreateBody,
  760. user: dict = Depends(require_admin_or_super_admin),
  761. db: Session = Depends(get_db),
  762. ):
  763. try:
  764. tenant_no = _get_tenant_no(db, user)
  765. obj = output_type_svc.create_output_type(
  766. db,
  767. type_code=body.type_code,
  768. type_name=body.type_name,
  769. tenant_no=tenant_no,
  770. order_num=body.order_num,
  771. remark=body.remark,
  772. user_id=user.get("user_id"),
  773. )
  774. return success_response(output_type_svc._row_to_dict(obj), message="创建成功")
  775. except ValueError as e:
  776. return error_response(400, str(e))
  777. @router.put("/output-types/{type_id}")
  778. async def update_output_type(
  779. type_id: str,
  780. body: OutputTypeUpdateBody,
  781. user: dict = Depends(require_admin_or_super_admin),
  782. db: Session = Depends(get_db),
  783. ):
  784. try:
  785. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  786. obj = output_type_svc.update_output_type(
  787. db,
  788. type_id,
  789. type_code=payload.get("type_code"),
  790. type_name=payload.get("type_name"),
  791. order_num=payload.get("order_num"),
  792. remark=payload.get("remark"),
  793. user_id=user.get("user_id"),
  794. )
  795. return success_response(output_type_svc._row_to_dict(obj), message="更新成功")
  796. except ValueError as e:
  797. return error_response(400, str(e))
  798. @router.delete("/output-types/{type_id}")
  799. async def delete_output_type(
  800. type_id: str,
  801. _user: dict = Depends(require_admin_or_super_admin),
  802. db: Session = Depends(get_db),
  803. ):
  804. try:
  805. output_type_svc.soft_delete_output_type(db, type_id)
  806. return success_response(None, message="删除成功")
  807. except ValueError as e:
  808. return error_response(400, str(e))
  809. # ---------------------------------------------------------------------------
  810. # 6. 耗材包(主 + 子项)
  811. # ---------------------------------------------------------------------------
  812. class PackItemInput(BaseModel):
  813. model_config = ConfigDict(populate_by_name=True)
  814. consumable_code: Optional[str] = Field(None, alias="consumableCode")
  815. consumable_name: Optional[str] = Field(None, alias="consumableName")
  816. spec_model: Optional[str] = Field(None, alias="specModel")
  817. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  818. unit: Optional[str] = None
  819. remark: Optional[str] = None
  820. class ConsumablePackCreateBody(BaseModel):
  821. model_config = ConfigDict(populate_by_name=True)
  822. pack_code: str = Field(..., alias="packCode")
  823. pack_name: str = Field(..., alias="packName")
  824. usage_scene: Optional[str] = Field(None, alias="usageScene")
  825. pack_type: Optional[str] = Field(None, alias="packType")
  826. status: Optional[int] = 1
  827. order_num: Optional[int] = Field(0, alias="orderNum")
  828. remark: Optional[str] = None
  829. items: List[PackItemInput] = []
  830. class ConsumablePackUpdateBody(BaseModel):
  831. model_config = ConfigDict(populate_by_name=True)
  832. pack_code: Optional[str] = Field(None, alias="packCode")
  833. pack_name: Optional[str] = Field(None, alias="packName")
  834. usage_scene: Optional[str] = Field(None, alias="usageScene")
  835. pack_type: Optional[str] = Field(None, alias="packType")
  836. status: Optional[int] = None
  837. order_num: Optional[int] = Field(None, alias="orderNum")
  838. remark: Optional[str] = None
  839. items: Optional[List[PackItemInput]] = None
  840. @router.get("/consumable-packs")
  841. async def list_consumable_packs(
  842. page: int = Query(1, ge=1),
  843. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  844. keyword: Optional[str] = None,
  845. user: dict = Depends(require_menu_code("production:consumable-pack:list")),
  846. db: Session = Depends(get_db),
  847. ):
  848. tenant_no = _get_tenant_no(db, user)
  849. items, total = consumable_pack_svc.list_consumable_packs(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  850. tp = consumable_pack_svc.total_pages(total, pageSize)
  851. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  852. @router.get("/consumable-packs/all")
  853. async def list_all_consumable_packs(
  854. user: dict = Depends(require_admin_or_super_admin),
  855. db: Session = Depends(get_db),
  856. ):
  857. tenant_no = _get_tenant_no(db, user)
  858. items = consumable_pack_svc.list_all_packs(db, tenant_no, user_info=user)
  859. return success_response(items)
  860. @router.get("/consumable-packs/{pack_id}")
  861. async def get_consumable_pack_detail(
  862. pack_id: str,
  863. user: dict = Depends(require_admin_or_super_admin),
  864. db: Session = Depends(get_db),
  865. ):
  866. detail = consumable_pack_svc.get_pack_detail(db, pack_id)
  867. if not detail:
  868. return error_response(404, "耗材包不存在")
  869. return success_response(detail)
  870. @router.post("/consumable-packs")
  871. async def create_consumable_pack(
  872. body: ConsumablePackCreateBody,
  873. user: dict = Depends(require_admin_or_super_admin),
  874. db: Session = Depends(get_db),
  875. ):
  876. try:
  877. tenant_no = _get_tenant_no(db, user)
  878. items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items else None
  879. obj = consumable_pack_svc.create_consumable_pack(
  880. db,
  881. pack_code=body.pack_code,
  882. pack_name=body.pack_name,
  883. usage_scene=body.usage_scene,
  884. pack_type=body.pack_type,
  885. status=body.status,
  886. items=items_data,
  887. tenant_no=tenant_no,
  888. order_num=body.order_num,
  889. remark=body.remark,
  890. user_id=user.get("user_id"),
  891. )
  892. return success_response(consumable_pack_svc._row_to_dict(obj), message="创建成功")
  893. except ValueError as e:
  894. return error_response(400, str(e))
  895. @router.put("/consumable-packs/{pack_id}")
  896. async def update_consumable_pack(
  897. pack_id: str,
  898. body: ConsumablePackUpdateBody,
  899. user: dict = Depends(require_admin_or_super_admin),
  900. db: Session = Depends(get_db),
  901. ):
  902. try:
  903. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  904. items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items is not None else None
  905. obj = consumable_pack_svc.update_consumable_pack(
  906. db,
  907. pack_id,
  908. pack_code=payload.get("pack_code"),
  909. pack_name=payload.get("pack_name"),
  910. usage_scene=payload.get("usage_scene"),
  911. pack_type=payload.get("pack_type"),
  912. status=payload.get("status"),
  913. items=items_data,
  914. order_num=payload.get("order_num"),
  915. remark=payload.get("remark"),
  916. user_id=user.get("user_id"),
  917. )
  918. return success_response(consumable_pack_svc._row_to_dict(obj), message="更新成功")
  919. except ValueError as e:
  920. return error_response(400, str(e))
  921. @router.delete("/consumable-packs/{pack_id}")
  922. async def delete_consumable_pack(
  923. pack_id: str,
  924. _user: dict = Depends(require_admin_or_super_admin),
  925. db: Session = Depends(get_db),
  926. ):
  927. try:
  928. consumable_pack_svc.soft_delete_consumable_pack(db, pack_id)
  929. return success_response(None, message="删除成功")
  930. except ValueError as e:
  931. return error_response(400, str(e))
  932. # ---------------------------------------------------------------------------
  933. # 7. 工艺库(主 + 3 个子表:试剂、耗材、产出物)
  934. # ---------------------------------------------------------------------------
  935. class ProcessReagentInput(BaseModel):
  936. model_config = ConfigDict(populate_by_name=True)
  937. reagent_code: Optional[str] = Field(None, alias="reagentCode")
  938. reagent_name: Optional[str] = Field(None, alias="reagentName")
  939. spec: Optional[str] = None
  940. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  941. unit: Optional[str] = None
  942. is_key_material: Optional[str] = Field("0", alias="isKeyMaterial")
  943. remark: Optional[str] = None
  944. class ProcessConsumableInput(BaseModel):
  945. model_config = ConfigDict(populate_by_name=True)
  946. consumable_code: Optional[str] = Field(None, alias="consumableCode")
  947. consumable_name: Optional[str] = Field(None, alias="consumableName")
  948. spec_model: Optional[str] = Field(None, alias="specModel")
  949. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  950. unit: Optional[str] = None
  951. is_required: Optional[str] = Field("1", alias="isRequired")
  952. remark: Optional[str] = None
  953. class ProcessOutputInput(BaseModel):
  954. model_config = ConfigDict(populate_by_name=True)
  955. output_type_id: Optional[str] = Field(None, alias="outputTypeId")
  956. output_name: Optional[str] = Field(None, alias="outputName")
  957. output_barcode: Optional[str] = Field(None, alias="outputBarcode")
  958. remark: Optional[str] = None
  959. class ProcessDataCollectionInput(BaseModel):
  960. model_config = ConfigDict(populate_by_name=True)
  961. collection_name: str = Field(..., alias="collectionName")
  962. unit: Optional[str] = None
  963. order_num: Optional[int] = Field(0, alias="orderNum")
  964. remark: Optional[str] = None
  965. class ProcessCreateBody(BaseModel):
  966. model_config = ConfigDict(populate_by_name=True)
  967. process_code: str = Field(..., alias="processCode")
  968. category_id: Optional[str] = Field(None, alias="categoryId")
  969. process_name: str = Field(..., alias="processName")
  970. description: Optional[str] = None
  971. equipment_name: Optional[str] = Field(None, alias="equipmentName")
  972. estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
  973. sop_ids: Optional[str] = Field(None, alias="sopIds")
  974. order_num: Optional[int] = Field(0, alias="orderNum")
  975. remark: Optional[str] = None
  976. reagents: List[ProcessReagentInput] = []
  977. consumables: List[ProcessConsumableInput] = []
  978. outputs: List[ProcessOutputInput] = []
  979. data_collections: List[ProcessDataCollectionInput] = Field([], alias="dataCollections")
  980. class ProcessUpdateBody(BaseModel):
  981. model_config = ConfigDict(populate_by_name=True)
  982. process_code: Optional[str] = Field(None, alias="processCode")
  983. category_id: Optional[str] = Field(None, alias="categoryId")
  984. process_name: Optional[str] = Field(None, alias="processName")
  985. description: Optional[str] = None
  986. equipment_name: Optional[str] = Field(None, alias="equipmentName")
  987. estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
  988. sop_ids: Optional[str] = Field(None, alias="sopIds")
  989. order_num: Optional[int] = Field(None, alias="orderNum")
  990. remark: Optional[str] = None
  991. reagents: Optional[List[ProcessReagentInput]] = None
  992. consumables: Optional[List[ProcessConsumableInput]] = None
  993. outputs: Optional[List[ProcessOutputInput]] = None
  994. data_collections: Optional[List[ProcessDataCollectionInput]] = Field(None, alias="dataCollections")
  995. @router.get("/processes")
  996. async def list_processes(
  997. page: int = Query(1, ge=1),
  998. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  999. keyword: Optional[str] = None,
  1000. user: dict = Depends(require_menu_code("production:process:list")),
  1001. db: Session = Depends(get_db),
  1002. ):
  1003. tenant_no = _get_tenant_no(db, user)
  1004. items, total = process_svc.list_processes(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  1005. tp = process_svc.total_pages(total, pageSize)
  1006. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1007. @router.get("/processes/all")
  1008. async def list_all_processes(
  1009. user: dict = Depends(require_admin_or_super_admin),
  1010. db: Session = Depends(get_db),
  1011. ):
  1012. tenant_no = _get_tenant_no(db, user)
  1013. items = process_svc.list_all_processes(db, tenant_no, user_info=user)
  1014. return success_response(items)
  1015. @router.get("/processes/{process_id}")
  1016. async def get_process_detail(
  1017. process_id: str,
  1018. user: dict = Depends(require_admin_or_super_admin),
  1019. db: Session = Depends(get_db),
  1020. ):
  1021. detail = process_svc.get_process_detail(db, process_id)
  1022. if not detail:
  1023. return error_response(404, "工艺不存在")
  1024. return success_response(detail)
  1025. @router.post("/processes")
  1026. async def create_process(
  1027. body: ProcessCreateBody,
  1028. user: dict = Depends(require_admin_or_super_admin),
  1029. db: Session = Depends(get_db),
  1030. ):
  1031. try:
  1032. tenant_no = _get_tenant_no(db, user)
  1033. reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents else None
  1034. consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables else None
  1035. outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs else None
  1036. data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections else None
  1037. # Convert string fields to appropriate types for sub-records
  1038. for rd in (reagents_data or []):
  1039. if "is_key_material" in rd and isinstance(rd["is_key_material"], str):
  1040. rd["is_key_material"] = int(rd["is_key_material"])
  1041. for cd in (consumables_data or []):
  1042. if "is_required" in cd and isinstance(cd["is_required"], str):
  1043. cd["is_required"] = int(cd["is_required"])
  1044. obj = process_svc.create_process(
  1045. db,
  1046. process_code=body.process_code,
  1047. category_id=body.category_id,
  1048. process_name=body.process_name,
  1049. description=body.description,
  1050. equipment_name=body.equipment_name,
  1051. estimated_duration=body.estimated_duration,
  1052. sop_ids=body.sop_ids,
  1053. reagents=reagents_data,
  1054. consumables=consumables_data,
  1055. outputs=outputs_data,
  1056. data_collections=data_collections_data,
  1057. tenant_no=tenant_no,
  1058. order_num=body.order_num,
  1059. remark=body.remark,
  1060. user_id=user.get("user_id"),
  1061. )
  1062. return success_response(process_svc._row_to_dict(obj), message="创建成功")
  1063. except ValueError as e:
  1064. return error_response(400, str(e))
  1065. @router.put("/processes/{process_id}")
  1066. async def update_process(
  1067. process_id: str,
  1068. body: ProcessUpdateBody,
  1069. user: dict = Depends(require_admin_or_super_admin),
  1070. db: Session = Depends(get_db),
  1071. ):
  1072. try:
  1073. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1074. reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents is not None else None
  1075. consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables is not None else None
  1076. outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs is not None else None
  1077. data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections is not None else None
  1078. # Convert string fields to appropriate types for sub-records
  1079. for rd in (reagents_data or []):
  1080. if "is_key_material" in rd and isinstance(rd["is_key_material"], str):
  1081. rd["is_key_material"] = int(rd["is_key_material"])
  1082. for cd in (consumables_data or []):
  1083. if "is_required" in cd and isinstance(cd["is_required"], str):
  1084. cd["is_required"] = int(cd["is_required"])
  1085. obj = process_svc.update_process(
  1086. db,
  1087. process_id,
  1088. process_code=payload.get("process_code"),
  1089. category_id=payload.get("category_id"),
  1090. process_name=payload.get("process_name"),
  1091. description=payload.get("description"),
  1092. equipment_name=payload.get("equipment_name"),
  1093. estimated_duration=payload.get("estimated_duration"),
  1094. sop_ids=payload.get("sop_ids"),
  1095. reagents=reagents_data,
  1096. consumables=consumables_data,
  1097. outputs=outputs_data,
  1098. data_collections=data_collections_data,
  1099. order_num=payload.get("order_num"),
  1100. remark=payload.get("remark"),
  1101. user_id=user.get("user_id"),
  1102. )
  1103. return success_response(process_svc._row_to_dict(obj), message="更新成功")
  1104. except ValueError as e:
  1105. return error_response(400, str(e))
  1106. @router.delete("/processes/{process_id}")
  1107. async def delete_process(
  1108. process_id: str,
  1109. _user: dict = Depends(require_admin_or_super_admin),
  1110. db: Session = Depends(get_db),
  1111. ):
  1112. try:
  1113. process_svc.soft_delete_process(db, process_id)
  1114. return success_response(None, message="删除成功")
  1115. except ValueError as e:
  1116. return error_response(400, str(e))
  1117. # ---------------------------------------------------------------------------
  1118. # 8. 工艺路线管理
  1119. # ---------------------------------------------------------------------------
  1120. @router.get("/process-routes")
  1121. async def list_process_routes(
  1122. page: int = Query(1, ge=1),
  1123. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1124. routeName: Optional[str] = None,
  1125. routeCode: Optional[str] = None,
  1126. productTypeId: Optional[str] = None,
  1127. user: dict = Depends(require_menu_code("production:process-route:list")),
  1128. db: Session = Depends(get_db),
  1129. ):
  1130. tenant_no = _get_tenant_no(db, user)
  1131. items, total = route_svc.list_routes(
  1132. db, page=page, page_size=pageSize,
  1133. route_name=routeName, route_code=routeCode,
  1134. product_type_id=productTypeId, tenant_no=tenant_no,
  1135. user_info=user,
  1136. )
  1137. tp = route_svc.total_pages(total, pageSize)
  1138. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1139. @router.get("/process-routes/{route_id}")
  1140. async def get_process_route_detail(
  1141. route_id: str,
  1142. _user: dict = Depends(require_admin_or_super_admin),
  1143. db: Session = Depends(get_db),
  1144. ):
  1145. detail = route_svc.get_route_detail(db, route_id)
  1146. if not detail:
  1147. return error_response(404, "路线不存在")
  1148. return success_response(detail)
  1149. @router.post("/process-routes")
  1150. async def create_process_route(
  1151. body: ProcessRouteCreateBody,
  1152. user: dict = Depends(require_admin_or_super_admin),
  1153. db: Session = Depends(get_db),
  1154. ):
  1155. try:
  1156. tenant_no = _get_tenant_no(db, user)
  1157. obj = route_svc.create_route(
  1158. db,
  1159. route_code=body.route_code,
  1160. route_name=body.route_name,
  1161. product_type_id=body.product_type_id,
  1162. version=body.version,
  1163. remark=body.remark,
  1164. tenant_no=tenant_no,
  1165. user_id=user.get("user_id"),
  1166. )
  1167. return success_response(route_svc._route_row_to_dict(obj), message="创建成功")
  1168. except ValueError as e:
  1169. return error_response(400, str(e))
  1170. @router.put("/process-routes/{route_id}")
  1171. async def update_process_route(
  1172. route_id: str,
  1173. body: ProcessRouteUpdateBody,
  1174. user: dict = Depends(require_admin_or_super_admin),
  1175. db: Session = Depends(get_db),
  1176. ):
  1177. try:
  1178. payload = body.model_dump(exclude_unset=True, by_alias=False)
  1179. obj = route_svc.update_route(
  1180. db, route_id,
  1181. user_id=user.get("user_id"),
  1182. route_code=payload.get("route_code"),
  1183. route_name=payload.get("route_name"),
  1184. product_type_id=payload.get("product_type_id"),
  1185. version=payload.get("version"),
  1186. remark=payload.get("remark"),
  1187. )
  1188. return success_response(route_svc._route_row_to_dict(obj), message="更新成功")
  1189. except ValueError as e:
  1190. return error_response(400, str(e))
  1191. @router.put("/process-routes/set-current")
  1192. async def set_current_process_route(
  1193. body: SetCurrentBody,
  1194. user: dict = Depends(require_admin_or_super_admin),
  1195. db: Session = Depends(get_db),
  1196. ):
  1197. try:
  1198. tenant_no = _get_tenant_no(db, user)
  1199. obj = route_svc.set_current_route(db, body.route_id, tenant_no)
  1200. return success_response(route_svc._route_row_to_dict(obj), message="设置成功")
  1201. except ValueError as e:
  1202. return error_response(400, str(e))
  1203. @router.delete("/process-routes/{route_id}")
  1204. async def delete_process_route(
  1205. route_id: str,
  1206. _user: dict = Depends(require_admin_or_super_admin),
  1207. db: Session = Depends(get_db),
  1208. ):
  1209. try:
  1210. route_svc.soft_delete_route(db, route_id)
  1211. return success_response(None, message="删除成功")
  1212. except ValueError as e:
  1213. return error_response(400, str(e))
  1214. # ----- 步骤操作 -----
  1215. @router.post("/process-routes/step/add")
  1216. async def add_route_step(
  1217. body: StepAddBody,
  1218. user: dict = Depends(require_admin_or_super_admin),
  1219. db: Session = Depends(get_db),
  1220. ):
  1221. try:
  1222. tenant_no = _get_tenant_no(db, user)
  1223. step_data = route_svc.add_step(
  1224. db,
  1225. route_id=body.route_id,
  1226. process_id=body.process_id,
  1227. step_order=body.step_order,
  1228. tenant_no=tenant_no,
  1229. user_id=user.get("user_id"),
  1230. )
  1231. return success_response(step_data, message="添加成功")
  1232. except ValueError as e:
  1233. return error_response(400, str(e))
  1234. @router.post("/process-routes/step/copy")
  1235. async def copy_route_step(
  1236. body: StepCopyBody,
  1237. user: dict = Depends(require_admin_or_super_admin),
  1238. db: Session = Depends(get_db),
  1239. ):
  1240. try:
  1241. tenant_no = _get_tenant_no(db, user)
  1242. step_data = route_svc.copy_step(
  1243. db,
  1244. step_id=body.step_id,
  1245. tenant_no=tenant_no,
  1246. user_id=user.get("user_id"),
  1247. )
  1248. return success_response(step_data, message="复制成功")
  1249. except ValueError as e:
  1250. return error_response(400, str(e))
  1251. @router.put("/process-routes/step/update")
  1252. async def update_route_step(
  1253. body: StepUpdateBody,
  1254. user: dict = Depends(require_admin_or_super_admin),
  1255. db: Session = Depends(get_db),
  1256. ):
  1257. try:
  1258. payload = body.model_dump(exclude_unset=True, by_alias=False)
  1259. payload.pop("step_id", None)
  1260. obj = route_svc.update_step(db, body.step_id, user_id=user.get("user_id"), **payload)
  1261. return success_response(route_svc._step_row_to_dict(obj), message="更新成功")
  1262. except ValueError as e:
  1263. return error_response(400, str(e))
  1264. @router.put("/process-routes/step/reorder")
  1265. async def reorder_route_steps(
  1266. body: StepReorderBody,
  1267. _user: dict = Depends(require_admin_or_super_admin),
  1268. db: Session = Depends(get_db),
  1269. ):
  1270. try:
  1271. route_svc.reorder_steps(db, body.route_id, body.step_ids)
  1272. return success_response(None, message="排序成功")
  1273. except ValueError as e:
  1274. return error_response(400, str(e))
  1275. @router.delete("/process-routes/step/{step_id}")
  1276. async def delete_route_step(
  1277. step_id: str,
  1278. _user: dict = Depends(require_admin_or_super_admin),
  1279. db: Session = Depends(get_db),
  1280. ):
  1281. try:
  1282. route_svc.soft_delete_step(db, step_id)
  1283. return success_response(None, message="删除成功")
  1284. except ValueError as e:
  1285. return error_response(400, str(e))
  1286. # ----- 步骤子表 -----
  1287. @router.put("/process-routes/step/reagent/update")
  1288. async def update_step_reagents(
  1289. body: StepReagentUpdateBody,
  1290. user: dict = Depends(require_admin_or_super_admin),
  1291. db: Session = Depends(get_db),
  1292. ):
  1293. try:
  1294. tenant_no = _get_tenant_no(db, user)
  1295. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1296. route_svc.update_step_reagents(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1297. return success_response(None, message="更新成功")
  1298. except ValueError as e:
  1299. return error_response(400, str(e))
  1300. @router.put("/process-routes/step/consumable/update")
  1301. async def update_step_consumables(
  1302. body: StepConsumableUpdateBody,
  1303. user: dict = Depends(require_admin_or_super_admin),
  1304. db: Session = Depends(get_db),
  1305. ):
  1306. try:
  1307. tenant_no = _get_tenant_no(db, user)
  1308. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1309. route_svc.update_step_consumables(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1310. return success_response(None, message="更新成功")
  1311. except ValueError as e:
  1312. return error_response(400, str(e))
  1313. @router.put("/process-routes/step/output/update")
  1314. async def update_step_outputs(
  1315. body: StepOutputUpdateBody,
  1316. user: dict = Depends(require_admin_or_super_admin),
  1317. db: Session = Depends(get_db),
  1318. ):
  1319. try:
  1320. tenant_no = _get_tenant_no(db, user)
  1321. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1322. route_svc.update_step_outputs(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1323. return success_response(None, message="更新成功")
  1324. except ValueError as e:
  1325. return error_response(400, str(e))
  1326. @router.put("/process-routes/step/data-collection/update")
  1327. async def update_step_data_collections(
  1328. body: StepDataCollectionUpdateBody,
  1329. user: dict = Depends(require_admin_or_super_admin),
  1330. db: Session = Depends(get_db),
  1331. ):
  1332. try:
  1333. tenant_no = _get_tenant_no(db, user)
  1334. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1335. route_svc.update_step_data_collections(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1336. return success_response(None, message="更新成功")
  1337. except ValueError as e:
  1338. return error_response(400, str(e))
  1339. # ---------------------------------------------------------------------------
  1340. # 9. 入库生产工单
  1341. # ---------------------------------------------------------------------------
  1342. @router.get("/production-work-orders")
  1343. async def list_production_work_orders(
  1344. page: int = Query(1, ge=1),
  1345. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1346. workOrderNo: Optional[str] = None,
  1347. workOrderStatus: Optional[str] = None,
  1348. protocolNo: Optional[str] = None,
  1349. sampleCode: Optional[str] = None,
  1350. user: dict = Depends(require_admin_or_super_admin),
  1351. db: Session = Depends(get_db),
  1352. ):
  1353. tenant_no = _get_tenant_no(db, user)
  1354. query = ProductionWorkOrderListQuery(
  1355. page=page, page_size=pageSize,
  1356. work_order_no=workOrderNo, work_order_status=workOrderStatus,
  1357. protocol_no=protocolNo, sample_code=sampleCode,
  1358. )
  1359. result = production_work_order_svc.list_production_work_orders(
  1360. db, tenant_no=tenant_no, query=query, page=page, page_size=pageSize,
  1361. user_info=user,
  1362. )
  1363. items = result.get("items", [])
  1364. total = result.get("total", 0)
  1365. tp = production_task_svc.total_pages(total, pageSize)
  1366. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1367. @router.get("/production-work-orders/{wo_id}")
  1368. async def get_production_work_order(
  1369. wo_id: str,
  1370. user: dict = Depends(require_admin_or_super_admin),
  1371. db: Session = Depends(get_db),
  1372. ):
  1373. tenant_no = _get_tenant_no(db, user)
  1374. wo = production_work_order_svc.get_production_work_order(db, wo_id, tenant_no)
  1375. if not wo:
  1376. return error_response(404, "工单不存在")
  1377. return success_response(production_work_order_svc._wo_to_response_dict(db, wo))
  1378. @router.put("/production-work-orders/{wo_id}/complete")
  1379. async def complete_production_work_order(
  1380. wo_id: str,
  1381. body: ProductionWorkOrderComplete,
  1382. user: dict = Depends(require_admin_or_super_admin),
  1383. db: Session = Depends(get_db),
  1384. ):
  1385. try:
  1386. tenant_no = _get_tenant_no(db, user)
  1387. wo = production_work_order_svc.complete_production_work_order(
  1388. db, wo_id, tenant_no, user.get("user_id"), body,
  1389. )
  1390. return success_response(production_work_order_svc._wo_to_response_dict(db, wo), message="操作成功")
  1391. except ValueError as e:
  1392. return error_response(400, str(e))
  1393. # ---------------------------------------------------------------------------
  1394. # 10. 生产任务
  1395. # ---------------------------------------------------------------------------
  1396. @router.post("/production-tasks")
  1397. async def create_production_task(
  1398. body: ProductionTaskCreate,
  1399. user: dict = Depends(require_admin_or_super_admin),
  1400. db: Session = Depends(get_db),
  1401. ):
  1402. try:
  1403. tenant_no = _get_tenant_no(db, user)
  1404. data = body.model_dump(by_alias=False)
  1405. task = production_task_svc.create_production_task(
  1406. db, tenant_no=tenant_no, user_id=user.get("user_id"),
  1407. create_dept_id="", data=data,
  1408. )
  1409. db.commit()
  1410. return success_response(production_task_svc._task_to_dict(task), message="创建成功")
  1411. except ValueError as e:
  1412. return error_response(400, str(e))
  1413. @router.get("/production-tasks")
  1414. async def list_production_tasks(
  1415. page: int = Query(1, ge=1),
  1416. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1417. workOrderId: Optional[str] = None,
  1418. taskNo: Optional[str] = None,
  1419. taskStatus: Optional[str] = None,
  1420. user: dict = Depends(require_admin_or_super_admin),
  1421. db: Session = Depends(get_db),
  1422. ):
  1423. tenant_no = _get_tenant_no(db, user)
  1424. items, total = production_task_svc.list_production_tasks(
  1425. db, tenant_no=tenant_no, page=page, page_size=pageSize,
  1426. work_order_id=workOrderId, task_no=taskNo, task_status=taskStatus,
  1427. user_info=user,
  1428. )
  1429. tp = production_task_svc.total_pages(total, pageSize)
  1430. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1431. @router.get("/production-tasks/{task_id}")
  1432. async def get_production_task_detail(
  1433. task_id: str,
  1434. user: dict = Depends(require_admin_or_super_admin),
  1435. db: Session = Depends(get_db),
  1436. ):
  1437. tenant_no = _get_tenant_no(db, user)
  1438. detail = production_task_svc.get_production_task_detail(db, task_id, tenant_no)
  1439. if not detail:
  1440. return error_response(404, "任务不存在")
  1441. return success_response(detail)
  1442. @router.put("/production-tasks/{task_id}")
  1443. async def update_production_task(
  1444. task_id: str,
  1445. body: ProductionTaskUpdate,
  1446. user: dict = Depends(require_admin_or_super_admin),
  1447. db: Session = Depends(get_db),
  1448. ):
  1449. try:
  1450. tenant_no = _get_tenant_no(db, user)
  1451. data = body.model_dump(exclude_unset=True, by_alias=False)
  1452. task = production_task_svc.update_production_task(
  1453. db, task_id, tenant_no, user.get("user_id"), data,
  1454. )
  1455. return success_response(production_task_svc._task_to_dict(task), message="更新成功")
  1456. except ValueError as e:
  1457. return error_response(400, str(e))
  1458. @router.delete("/production-tasks/{task_id}")
  1459. async def delete_production_task(
  1460. task_id: str,
  1461. user: dict = Depends(require_admin_or_super_admin),
  1462. db: Session = Depends(get_db),
  1463. ):
  1464. try:
  1465. tenant_no = _get_tenant_no(db, user)
  1466. production_task_svc.delete_production_task(db, task_id, tenant_no, user.get("user_id"))
  1467. return success_response(None, message="删除成功")
  1468. except ValueError as e:
  1469. return error_response(400, str(e))
  1470. @router.put("/production-tasks/{task_id}/status")
  1471. async def update_production_task_status(
  1472. task_id: str,
  1473. body: ProductionTaskStatusUpdate,
  1474. user: dict = Depends(require_admin_or_super_admin),
  1475. db: Session = Depends(get_db),
  1476. ):
  1477. try:
  1478. tenant_no = _get_tenant_no(db, user)
  1479. task = production_task_svc.update_task_status(
  1480. db, task_id, tenant_no, user.get("user_id"), body.task_status,
  1481. )
  1482. return success_response(production_task_svc._task_to_dict(task), message="操作成功")
  1483. except ValueError as e:
  1484. return error_response(400, str(e))
  1485. @router.post("/production-tasks/{task_id}/steps")
  1486. async def add_production_task_step(
  1487. task_id: str,
  1488. body: TaskStepAdd,
  1489. user: dict = Depends(require_admin_or_super_admin),
  1490. db: Session = Depends(get_db),
  1491. ):
  1492. try:
  1493. tenant_no = _get_tenant_no(db, user)
  1494. data = body.model_dump(by_alias=False)
  1495. step_data = production_task_svc.add_task_step(
  1496. db, task_id, tenant_no, user.get("user_id"), data,
  1497. )
  1498. return success_response(step_data, message="添加成功")
  1499. except ValueError as e:
  1500. return error_response(400, str(e))
  1501. @router.put("/production-tasks/{task_id}/steps/{step_id}")
  1502. async def update_production_task_step(
  1503. task_id: str,
  1504. step_id: str,
  1505. body: TaskStepUpdate,
  1506. user: dict = Depends(require_admin_or_super_admin),
  1507. db: Session = Depends(get_db),
  1508. ):
  1509. try:
  1510. tenant_no = _get_tenant_no(db, user)
  1511. data = body.model_dump(exclude_unset=True, by_alias=False)
  1512. step = production_task_svc.update_task_step(
  1513. db, step_id, tenant_no, user.get("user_id"), data,
  1514. )
  1515. return success_response(production_task_svc._step_to_dict(step), message="更新成功")
  1516. except ValueError as e:
  1517. return error_response(400, str(e))
  1518. @router.delete("/production-tasks/{task_id}/steps/{step_id}")
  1519. async def delete_production_task_step(
  1520. task_id: str,
  1521. step_id: str,
  1522. user: dict = Depends(require_admin_or_super_admin),
  1523. db: Session = Depends(get_db),
  1524. ):
  1525. try:
  1526. tenant_no = _get_tenant_no(db, user)
  1527. production_task_svc.delete_task_step(db, step_id, tenant_no, user.get("user_id"))
  1528. return success_response(None, message="删除成功")
  1529. except ValueError as e:
  1530. return error_response(400, str(e))
  1531. @router.put("/production-tasks/step/reorder")
  1532. async def reorder_production_task_steps(
  1533. body: TaskStepReorderBody,
  1534. _user: dict = Depends(require_admin_or_super_admin),
  1535. db: Session = Depends(get_db),
  1536. ):
  1537. try:
  1538. production_task_svc.reorder_task_steps(db, body.task_id, body.step_ids)
  1539. return success_response(None, message="排序成功")
  1540. except ValueError as e:
  1541. return error_response(400, str(e))
  1542. @router.post("/production-tasks/{task_id}/steps/{step_id}/copy")
  1543. async def copy_production_task_step(
  1544. task_id: str,
  1545. step_id: str,
  1546. user: dict = Depends(require_admin_or_super_admin),
  1547. db: Session = Depends(get_db),
  1548. ):
  1549. try:
  1550. tenant_no = _get_tenant_no(db, user)
  1551. step_data = production_task_svc.copy_task_step(
  1552. db,
  1553. step_id=step_id,
  1554. tenant_no=tenant_no,
  1555. user_id=user.get("user_id"),
  1556. )
  1557. return success_response(step_data, message="复制成功")
  1558. except ValueError as e:
  1559. return error_response(400, str(e))
  1560. @router.put("/production-tasks/{task_id}/steps/{step_id}/reagents")
  1561. async def replace_task_step_reagents(
  1562. task_id: str,
  1563. step_id: str,
  1564. body: List[TaskStepMaterialItem],
  1565. user: dict = Depends(require_admin_or_super_admin),
  1566. db: Session = Depends(get_db),
  1567. ):
  1568. try:
  1569. tenant_no = _get_tenant_no(db, user)
  1570. items_data = []
  1571. for it in body:
  1572. d = it.model_dump(by_alias=False)
  1573. items_data.append({
  1574. "reagent_code": d.get("material_code"),
  1575. "reagent_name": d.get("material_name"),
  1576. "spec": d.get("spec"),
  1577. "standard_usage": d.get("standard_usage"),
  1578. "actual_usage": d.get("actual_usage"),
  1579. "unit": d.get("unit"),
  1580. "is_key_material": d.get("is_key_material", "0"),
  1581. "remark": d.get("remark"),
  1582. })
  1583. production_task_svc.replace_step_reagents(db, step_id, tenant_no, user.get("user_id"), items_data)
  1584. return success_response(None, message="更新成功")
  1585. except ValueError as e:
  1586. return error_response(400, str(e))
  1587. @router.put("/production-tasks/{task_id}/steps/{step_id}/consumables")
  1588. async def replace_task_step_consumables(
  1589. task_id: str,
  1590. step_id: str,
  1591. body: List[TaskStepMaterialItem],
  1592. user: dict = Depends(require_admin_or_super_admin),
  1593. db: Session = Depends(get_db),
  1594. ):
  1595. try:
  1596. tenant_no = _get_tenant_no(db, user)
  1597. items_data = []
  1598. for it in body:
  1599. d = it.model_dump(by_alias=False)
  1600. items_data.append({
  1601. "consumable_code": d.get("material_code"),
  1602. "consumable_name": d.get("material_name"),
  1603. "spec_model": d.get("spec_model"),
  1604. "standard_usage": d.get("standard_usage"),
  1605. "actual_usage": d.get("actual_usage"),
  1606. "unit": d.get("unit"),
  1607. "is_required": d.get("is_required", "1"),
  1608. "remark": d.get("remark"),
  1609. })
  1610. production_task_svc.replace_step_consumables(db, step_id, tenant_no, user.get("user_id"), items_data)
  1611. return success_response(None, message="更新成功")
  1612. except ValueError as e:
  1613. return error_response(400, str(e))
  1614. @router.put("/production-tasks/{task_id}/steps/{step_id}/data-collection")
  1615. async def replace_task_step_data_collections(
  1616. task_id: str,
  1617. step_id: str,
  1618. body: List[TaskStepDataCollectionItem],
  1619. user: dict = Depends(require_admin_or_super_admin),
  1620. db: Session = Depends(get_db),
  1621. ):
  1622. try:
  1623. tenant_no = _get_tenant_no(db, user)
  1624. items_data = [it.model_dump(by_alias=False) for it in body]
  1625. production_task_svc.replace_step_data_collections(db, step_id, tenant_no, user.get("user_id"), items_data)
  1626. return success_response(None, message="更新成功")
  1627. except ValueError as e:
  1628. return error_response(400, str(e))
  1629. @router.put("/production-tasks/{task_id}/steps/{step_id}/outputs")
  1630. async def replace_task_step_outputs(
  1631. task_id: str,
  1632. step_id: str,
  1633. body: List[TaskStepOutputItem],
  1634. user: dict = Depends(require_admin_or_super_admin),
  1635. db: Session = Depends(get_db),
  1636. ):
  1637. try:
  1638. tenant_no = _get_tenant_no(db, user)
  1639. items_data = [it.model_dump(by_alias=False) for it in body]
  1640. production_task_svc.replace_step_outputs(db, step_id, tenant_no, user.get("user_id"), items_data)
  1641. return success_response(None, message="更新成功")
  1642. except ValueError as e:
  1643. return error_response(400, str(e))
  1644. @router.post("/production-tasks/{task_id}/inputs")
  1645. async def set_production_task_inputs(
  1646. task_id: str,
  1647. body: List[TaskInputItem],
  1648. user: dict = Depends(require_admin_or_super_admin),
  1649. db: Session = Depends(get_db),
  1650. ):
  1651. try:
  1652. tenant_no = _get_tenant_no(db, user)
  1653. items_data = [it.model_dump(by_alias=False) for it in body]
  1654. production_task_svc.set_task_inputs(db, task_id, tenant_no, user.get("user_id"), items_data)
  1655. return success_response(None, message="设置成功")
  1656. except ValueError as e:
  1657. return error_response(400, str(e))
  1658. @router.post("/production-tasks/{task_id}/outputs")
  1659. async def set_production_task_outputs(
  1660. task_id: str,
  1661. body: List[TaskOutputItem],
  1662. user: dict = Depends(require_admin_or_super_admin),
  1663. db: Session = Depends(get_db),
  1664. ):
  1665. try:
  1666. tenant_no = _get_tenant_no(db, user)
  1667. items_data = [it.model_dump(by_alias=False) for it in body]
  1668. production_task_svc.set_task_outputs(db, task_id, tenant_no, user.get("user_id"), items_data)
  1669. return success_response(None, message="设置成功")
  1670. except ValueError as e:
  1671. return error_response(400, str(e))
  1672. # ---------------------------------------------------------------------------
  1673. # 11. 细胞产品库
  1674. # ---------------------------------------------------------------------------
  1675. @router.get("/cell-products/next-code")
  1676. async def get_next_cell_product_code(
  1677. count: int = Query(1, ge=1, le=50),
  1678. user: dict = Depends(require_admin_or_super_admin),
  1679. db: Session = Depends(get_db),
  1680. ):
  1681. tenant_no = _get_tenant_no(db, user)
  1682. if count <= 1:
  1683. code = cell_product_svc.get_next_code(db, tenant_no)
  1684. return success_response({"code": code})
  1685. codes = cell_product_svc.reserve_next_codes(db, tenant_no, count)
  1686. return success_response({"codes": codes, "code": codes[0] if codes else ""})
  1687. @router.get("/cell-products")
  1688. async def list_cell_products(
  1689. page: int = Query(1, ge=1),
  1690. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1691. keyword: Optional[str] = None,
  1692. productCode: Optional[str] = None,
  1693. productName: Optional[str] = None,
  1694. productTypeId: Optional[str] = None,
  1695. productStatus: Optional[str] = None,
  1696. passage: Optional[str] = None,
  1697. user: dict = Depends(require_admin_or_super_admin),
  1698. db: Session = Depends(get_db),
  1699. ):
  1700. tenant_no = _get_tenant_no(db, user)
  1701. result = cell_product_svc.list_cell_products(
  1702. db, tenant_no=tenant_no, page=page, page_size=pageSize,
  1703. keyword=keyword,
  1704. product_code=productCode, product_name=productName,
  1705. product_type_id=productTypeId, product_status=productStatus,
  1706. passage=passage,
  1707. user_info=user,
  1708. )
  1709. items = result.get("items", [])
  1710. total = result.get("total", 0)
  1711. tp = cell_product_svc.total_pages(total, pageSize)
  1712. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1713. @router.get("/cell-products/{product_id}")
  1714. async def get_cell_product_detail(
  1715. product_id: str,
  1716. user: dict = Depends(require_admin_or_super_admin),
  1717. db: Session = Depends(get_db),
  1718. ):
  1719. tenant_no = _get_tenant_no(db, user)
  1720. obj = cell_product_svc.get_cell_product(db, product_id, tenant_no)
  1721. if not obj:
  1722. return error_response(404, "细胞产品不存在")
  1723. return success_response(cell_product_svc.product_to_response(db, tenant_no, obj))
  1724. @router.post("/cell-products")
  1725. async def create_cell_product(
  1726. body: CellProductCreate,
  1727. user: dict = Depends(require_admin_or_super_admin),
  1728. db: Session = Depends(get_db),
  1729. ):
  1730. try:
  1731. tenant_no = _get_tenant_no(db, user)
  1732. items = body.items or []
  1733. qty = int(body.quantity) if body.quantity is not None else 1
  1734. qty = max(1, min(qty, 50))
  1735. if items:
  1736. created = cell_product_svc.create_cell_products_batch(
  1737. db,
  1738. tenant_no=tenant_no,
  1739. user_id=user.get("user_id"),
  1740. create_dept_id="",
  1741. data=body,
  1742. items=items,
  1743. )
  1744. elif qty > 1:
  1745. created = cell_product_svc.create_cell_products_by_quantity(
  1746. db,
  1747. tenant_no=tenant_no,
  1748. user_id=user.get("user_id"),
  1749. create_dept_id="",
  1750. data=body,
  1751. count=qty,
  1752. )
  1753. else:
  1754. obj = cell_product_svc.create_cell_product(
  1755. db, tenant_no=tenant_no, user_id=user.get("user_id"),
  1756. create_dept_id="", data=body,
  1757. )
  1758. return success_response(
  1759. cell_product_svc.product_to_response(db, tenant_no, obj), message="创建成功"
  1760. )
  1761. resp_list = [
  1762. cell_product_svc.product_to_response(db, tenant_no, obj) for obj in created
  1763. ]
  1764. n = len(created)
  1765. return success_response(
  1766. {"list": resp_list, "count": n},
  1767. message=f"成功创建 {n} 份细胞产品",
  1768. )
  1769. except ValueError as e:
  1770. return error_response(400, str(e))
  1771. @router.put("/cell-products/{product_id}")
  1772. async def update_cell_product(
  1773. product_id: str,
  1774. body: CellProductUpdate,
  1775. user: dict = Depends(require_admin_or_super_admin),
  1776. db: Session = Depends(get_db),
  1777. ):
  1778. try:
  1779. tenant_no = _get_tenant_no(db, user)
  1780. obj = cell_product_svc.update_cell_product(
  1781. db, product_id, tenant_no, user.get("user_id"), data=body,
  1782. )
  1783. return success_response(
  1784. cell_product_svc.product_to_response(db, tenant_no, obj), message="更新成功"
  1785. )
  1786. except ValueError as e:
  1787. return error_response(400, str(e))
  1788. @router.delete("/cell-products/{product_id}")
  1789. async def delete_cell_product(
  1790. product_id: str,
  1791. user: dict = Depends(require_admin_or_super_admin),
  1792. db: Session = Depends(get_db),
  1793. ):
  1794. try:
  1795. tenant_no = _get_tenant_no(db, user)
  1796. cell_product_svc.delete_cell_product(db, product_id, tenant_no, user.get("user_id"))
  1797. return success_response(None, message="删除成功")
  1798. except ValueError as e:
  1799. return error_response(400, str(e))