production.py 106 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901
  1. """生产管理 API 路由(7 个子模块:耗材、试剂、SOP文档、工艺工序分类、产出物类型、耗材包、工艺库)"""
  2. from fastapi import APIRouter, Depends, Query, HTTPException
  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 (
  8. require_admin_or_super_admin,
  9. require_any_menu_code,
  10. require_menu_code,
  11. )
  12. from app.core.response import success_response, error_response
  13. from app.crud import consumable as consumable_svc
  14. from app.crud import reagent as reagent_svc
  15. from app.crud import consumable_stock_log as consumable_stock_log_svc
  16. from app.crud import reagent_stock_log as reagent_stock_log_svc
  17. from app.crud import sop_document as sop_document_svc
  18. from app.crud import process_category as process_category_svc
  19. from app.crud import equipment as equipment_svc
  20. from app.crud import output_type as output_type_svc
  21. from app.crud import production_product_type as production_product_type_svc
  22. from app.crud import consumable_pack as consumable_pack_svc
  23. from app.crud import process as process_svc
  24. from app.crud import process_route as route_svc
  25. from app.schemas.process_route import (
  26. ProcessRouteCreateBody,
  27. ProcessRouteUpdateBody,
  28. SetCurrentBody,
  29. StepAddBody,
  30. StepCopyBody,
  31. StepUpdateBody,
  32. StepReorderBody,
  33. StepReagentUpdateBody,
  34. StepConsumableUpdateBody,
  35. StepOutputUpdateBody,
  36. StepDataCollectionUpdateBody,
  37. )
  38. from app.models.models import SysUser
  39. from app.crud import production_work_order as production_work_order_svc
  40. from app.crud import production_task as production_task_svc
  41. from app.crud import cell_product as cell_product_svc
  42. from app.crud import qc_work_order as qc_work_order_svc
  43. from app.schemas.production_work_order import (
  44. ProductionWorkOrderListQuery,
  45. ProductionWorkOrderComplete,
  46. ProductionWorkOrderCreate,
  47. ProductionWorkOrderConfigureBody,
  48. ProductionWorkOrderUpdate,
  49. )
  50. from app.schemas.production_task import (
  51. ProductionTaskCreate,
  52. ProductionTaskUpdate,
  53. ProductionTaskStatusUpdate,
  54. ProductionTaskForwardBody,
  55. ProductionTaskAssigneeUpdate,
  56. ProductionTaskStepDispatchBody,
  57. SpawnTemporaryWorkOrderBody,
  58. TaskStepAdd,
  59. TaskStepUpdate,
  60. TaskStepMaterialItem,
  61. TaskStepOutputItem,
  62. TaskStepDataCollectionItem,
  63. TaskInputItem,
  64. TaskOutputItem,
  65. TaskStepReorderBody,
  66. ProductionTaskListByWorkOrdersBody,
  67. )
  68. from app.schemas.cell_product import CellProductCreate, CellProductUpdate
  69. from app.schemas.qc_work_order import ProductionQcWorkOrderCreateBody
  70. router = APIRouter(prefix="/web/admin", tags=["生产管理"])
  71. _LEGACY_MATERIAL_GONE_MSG = "耗材/试剂维护已迁移至【生产物料库】,请使用 production-material 接口"
  72. def _legacy_material_write_gone():
  73. return error_response(410, _LEGACY_MATERIAL_GONE_MSG)
  74. def _get_tenant_no(db: Session, user: dict) -> str:
  75. user_id = user.get("user_id")
  76. if not user_id:
  77. return "zkxw"
  78. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  79. return (sys_user.tenant_no if sys_user else None) or "zkxw"
  80. def _get_current_user_id(user: dict) -> str:
  81. return (user.get("user_id") or "").strip()
  82. # ---------------------------------------------------------------------------
  83. # 1. 耗材
  84. # ---------------------------------------------------------------------------
  85. class ConsumableCreateBody(BaseModel):
  86. model_config = ConfigDict(populate_by_name=True)
  87. consumable_code: str = Field(..., alias="consumableCode")
  88. consumable_name: str = Field(..., alias="consumableName")
  89. spec_model: Optional[str] = Field(None, alias="specModel")
  90. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  91. unit: Optional[str] = None
  92. description: Optional[str] = None
  93. warning_qty: Optional[float] = Field(0, alias="warningQty")
  94. status: Optional[str] = Field("1", alias="status")
  95. order_num: Optional[int] = Field(0, alias="orderNum")
  96. remark: Optional[str] = None
  97. class ConsumableUpdateBody(BaseModel):
  98. model_config = ConfigDict(populate_by_name=True)
  99. consumable_code: Optional[str] = Field(None, alias="consumableCode")
  100. consumable_name: Optional[str] = Field(None, alias="consumableName")
  101. spec_model: Optional[str] = Field(None, alias="specModel")
  102. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  103. unit: Optional[str] = None
  104. description: Optional[str] = None
  105. warning_qty: Optional[float] = Field(None, alias="warningQty")
  106. status: Optional[str] = Field(None, alias="status")
  107. order_num: Optional[int] = Field(None, alias="orderNum")
  108. remark: Optional[str] = None
  109. @router.get("/consumables")
  110. async def list_consumables(
  111. page: int = Query(1, ge=1),
  112. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  113. keyword: Optional[str] = None,
  114. user: dict = Depends(require_menu_code("production:consumable:list")),
  115. db: Session = Depends(get_db),
  116. ):
  117. tenant_no = _get_tenant_no(db, user)
  118. items, total = consumable_svc.list_consumables(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  119. tp = consumable_svc.total_pages(total, pageSize)
  120. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  121. @router.get("/consumables/all")
  122. async def list_all_consumables(
  123. user: dict = Depends(require_admin_or_super_admin),
  124. db: Session = Depends(get_db),
  125. ):
  126. tenant_no = _get_tenant_no(db, user)
  127. items, _ = consumable_svc.list_consumables(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  128. return success_response(items)
  129. @router.post("/consumables")
  130. async def create_consumable(
  131. body: ConsumableCreateBody,
  132. user: dict = Depends(require_admin_or_super_admin),
  133. db: Session = Depends(get_db),
  134. ):
  135. return _legacy_material_write_gone()
  136. @router.put("/consumables/{consumable_id}")
  137. async def update_consumable(
  138. consumable_id: str,
  139. body: ConsumableUpdateBody,
  140. user: dict = Depends(require_admin_or_super_admin),
  141. db: Session = Depends(get_db),
  142. ):
  143. return _legacy_material_write_gone()
  144. @router.delete("/consumables/{consumable_id}")
  145. async def delete_consumable(
  146. consumable_id: str,
  147. _user: dict = Depends(require_admin_or_super_admin),
  148. db: Session = Depends(get_db),
  149. ):
  150. return _legacy_material_write_gone()
  151. class StockInBody(BaseModel):
  152. model_config = ConfigDict(populate_by_name=True)
  153. qty: float = Field(..., gt=0)
  154. remark: Optional[str] = None
  155. class StockOutBody(BaseModel):
  156. model_config = ConfigDict(populate_by_name=True)
  157. qty: float = Field(..., gt=0)
  158. receiver: str = Field(..., min_length=1)
  159. purpose: Optional[str] = None
  160. remark: Optional[str] = None
  161. class BatchStockInItem(BaseModel):
  162. model_config = ConfigDict(populate_by_name=True)
  163. materialId: str
  164. qty: float = Field(..., gt=0)
  165. remark: Optional[str] = None
  166. class BatchStockInBody(BaseModel):
  167. model_config = ConfigDict(populate_by_name=True)
  168. items: List[BatchStockInItem]
  169. remark: Optional[str] = None
  170. class BatchStockOutItem(BaseModel):
  171. model_config = ConfigDict(populate_by_name=True)
  172. materialId: str
  173. qty: float = Field(..., gt=0)
  174. class BatchStockOutBody(BaseModel):
  175. model_config = ConfigDict(populate_by_name=True)
  176. items: List[BatchStockOutItem]
  177. receiver: str = Field(..., min_length=1)
  178. purpose: Optional[str] = None
  179. remark: Optional[str] = None
  180. @router.post("/consumables/{consumable_id}/stock-in")
  181. async def consumable_stock_in(
  182. consumable_id: str,
  183. body: StockInBody,
  184. user: dict = Depends(require_admin_or_super_admin),
  185. db: Session = Depends(get_db),
  186. ):
  187. try:
  188. tenant_no = _get_tenant_no(db, user)
  189. from decimal import Decimal
  190. obj = consumable_svc.stock_in(
  191. db, consumable_id,
  192. qty=Decimal(str(body.qty)),
  193. remark=body.remark,
  194. user_id=user.get("user_id"),
  195. tenant_no=tenant_no,
  196. )
  197. return success_response(consumable_svc._row_to_dict(obj), message="入库成功")
  198. except ValueError as e:
  199. return error_response(400, str(e))
  200. @router.post("/consumables/{consumable_id}/stock-out")
  201. async def consumable_stock_out(
  202. consumable_id: str,
  203. body: StockOutBody,
  204. user: dict = Depends(require_admin_or_super_admin),
  205. db: Session = Depends(get_db),
  206. ):
  207. try:
  208. tenant_no = _get_tenant_no(db, user)
  209. from decimal import Decimal
  210. obj = consumable_svc.stock_out(
  211. db, consumable_id,
  212. qty=Decimal(str(body.qty)),
  213. receiver=body.receiver,
  214. purpose=body.purpose,
  215. remark=body.remark,
  216. user_id=user.get("user_id"),
  217. tenant_no=tenant_no,
  218. )
  219. return success_response(consumable_svc._row_to_dict(obj), message="出库成功")
  220. except ValueError as e:
  221. return error_response(400, str(e))
  222. @router.post("/consumables/batch-stock-in")
  223. async def consumable_batch_stock_in(
  224. body: BatchStockInBody,
  225. user: dict = Depends(require_admin_or_super_admin),
  226. db: Session = Depends(get_db),
  227. ):
  228. try:
  229. tenant_no = _get_tenant_no(db, user)
  230. items_data = [it.model_dump() for it in body.items]
  231. count = consumable_svc.batch_stock_in(
  232. db, items=items_data, remark=body.remark,
  233. tenant_no=tenant_no, user_id=user.get("user_id"),
  234. )
  235. return success_response({"count": count}, message=f"成功入库 {count} 条")
  236. except ValueError as e:
  237. return error_response(400, str(e))
  238. @router.post("/consumables/batch-stock-out")
  239. async def consumable_batch_stock_out(
  240. body: BatchStockOutBody,
  241. user: dict = Depends(require_admin_or_super_admin),
  242. db: Session = Depends(get_db),
  243. ):
  244. try:
  245. tenant_no = _get_tenant_no(db, user)
  246. items_data = [it.model_dump() for it in body.items]
  247. count = consumable_svc.batch_stock_out(
  248. db, items=items_data,
  249. receiver=body.receiver, purpose=body.purpose, remark=body.remark,
  250. tenant_no=tenant_no, user_id=user.get("user_id"),
  251. )
  252. return success_response({"count": count}, message=f"成功出库 {count} 条")
  253. except ValueError as e:
  254. return error_response(400, str(e))
  255. @router.get("/consumables/{consumable_id}/stock-logs")
  256. async def list_consumable_stock_logs(
  257. consumable_id: str,
  258. page: int = Query(1, ge=1),
  259. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  260. changeType: Optional[str] = None,
  261. user: dict = Depends(require_admin_or_super_admin),
  262. db: Session = Depends(get_db),
  263. ):
  264. tenant_no = _get_tenant_no(db, user)
  265. items, total = consumable_stock_log_svc.list_stock_logs(
  266. db, tenant_no, page=page, page_size=pageSize,
  267. material_id=consumable_id, change_type=changeType,
  268. user_info=user,
  269. )
  270. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  271. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  272. @router.get("/consumables/stock-logs")
  273. async def list_all_consumable_stock_logs(
  274. page: int = Query(1, ge=1),
  275. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  276. keyword: Optional[str] = None,
  277. changeType: Optional[str] = None,
  278. startTime: Optional[str] = None,
  279. endTime: Optional[str] = None,
  280. user: dict = Depends(require_admin_or_super_admin),
  281. db: Session = Depends(get_db),
  282. ):
  283. tenant_no = _get_tenant_no(db, user)
  284. items, total = consumable_stock_log_svc.list_stock_logs(
  285. db, tenant_no, page=page, page_size=pageSize,
  286. keyword=keyword, change_type=changeType,
  287. start_time=startTime, end_time=endTime,
  288. user_info=user,
  289. )
  290. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  291. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  292. # ---------------------------------------------------------------------------
  293. # 2. 试剂
  294. # ---------------------------------------------------------------------------
  295. class ReagentCreateBody(BaseModel):
  296. model_config = ConfigDict(populate_by_name=True)
  297. reagent_code: str = Field(..., alias="reagentCode")
  298. reagent_name: str = Field(..., alias="reagentName")
  299. spec: Optional[str] = None
  300. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  301. unit: Optional[str] = None
  302. description: Optional[str] = None
  303. warning_qty: Optional[float] = Field(0, alias="warningQty")
  304. status: Optional[str] = Field("1", alias="status")
  305. order_num: Optional[int] = Field(0, alias="orderNum")
  306. remark: Optional[str] = None
  307. class ReagentUpdateBody(BaseModel):
  308. model_config = ConfigDict(populate_by_name=True)
  309. reagent_code: Optional[str] = Field(None, alias="reagentCode")
  310. reagent_name: Optional[str] = Field(None, alias="reagentName")
  311. spec: Optional[str] = None
  312. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  313. unit: Optional[str] = None
  314. description: Optional[str] = None
  315. warning_qty: Optional[float] = Field(None, alias="warningQty")
  316. status: Optional[str] = Field(None, alias="status")
  317. order_num: Optional[int] = Field(None, alias="orderNum")
  318. remark: Optional[str] = None
  319. @router.get("/reagents")
  320. async def list_reagents(
  321. page: int = Query(1, ge=1),
  322. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  323. keyword: Optional[str] = None,
  324. user: dict = Depends(require_menu_code("production:reagent:list")),
  325. db: Session = Depends(get_db),
  326. ):
  327. tenant_no = _get_tenant_no(db, user)
  328. items, total = reagent_svc.list_reagents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  329. tp = reagent_svc.total_pages(total, pageSize)
  330. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  331. @router.get("/reagents/all")
  332. async def list_all_reagents(
  333. user: dict = Depends(require_admin_or_super_admin),
  334. db: Session = Depends(get_db),
  335. ):
  336. tenant_no = _get_tenant_no(db, user)
  337. items, _ = reagent_svc.list_reagents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  338. return success_response(items)
  339. @router.post("/reagents")
  340. async def create_reagent(
  341. body: ReagentCreateBody,
  342. user: dict = Depends(require_admin_or_super_admin),
  343. db: Session = Depends(get_db),
  344. ):
  345. return _legacy_material_write_gone()
  346. @router.put("/reagents/{reagent_id}")
  347. async def update_reagent(
  348. reagent_id: str,
  349. body: ReagentUpdateBody,
  350. user: dict = Depends(require_admin_or_super_admin),
  351. db: Session = Depends(get_db),
  352. ):
  353. return _legacy_material_write_gone()
  354. @router.delete("/reagents/{reagent_id}")
  355. async def delete_reagent(
  356. reagent_id: str,
  357. _user: dict = Depends(require_admin_or_super_admin),
  358. db: Session = Depends(get_db),
  359. ):
  360. return _legacy_material_write_gone()
  361. @router.post("/reagents/{reagent_id}/stock-in")
  362. async def reagent_stock_in(
  363. reagent_id: str,
  364. body: StockInBody,
  365. user: dict = Depends(require_admin_or_super_admin),
  366. db: Session = Depends(get_db),
  367. ):
  368. try:
  369. tenant_no = _get_tenant_no(db, user)
  370. from decimal import Decimal
  371. obj = reagent_svc.stock_in(
  372. db, reagent_id,
  373. qty=Decimal(str(body.qty)),
  374. remark=body.remark,
  375. user_id=user.get("user_id"),
  376. tenant_no=tenant_no,
  377. )
  378. return success_response(reagent_svc._row_to_dict(obj), message="入库成功")
  379. except ValueError as e:
  380. return error_response(400, str(e))
  381. @router.post("/reagents/{reagent_id}/stock-out")
  382. async def reagent_stock_out(
  383. reagent_id: str,
  384. body: StockOutBody,
  385. user: dict = Depends(require_admin_or_super_admin),
  386. db: Session = Depends(get_db),
  387. ):
  388. try:
  389. tenant_no = _get_tenant_no(db, user)
  390. from decimal import Decimal
  391. obj = reagent_svc.stock_out(
  392. db, reagent_id,
  393. qty=Decimal(str(body.qty)),
  394. receiver=body.receiver,
  395. purpose=body.purpose,
  396. remark=body.remark,
  397. user_id=user.get("user_id"),
  398. tenant_no=tenant_no,
  399. )
  400. return success_response(reagent_svc._row_to_dict(obj), message="出库成功")
  401. except ValueError as e:
  402. return error_response(400, str(e))
  403. @router.post("/reagents/batch-stock-in")
  404. async def reagent_batch_stock_in(
  405. body: BatchStockInBody,
  406. user: dict = Depends(require_admin_or_super_admin),
  407. db: Session = Depends(get_db),
  408. ):
  409. try:
  410. tenant_no = _get_tenant_no(db, user)
  411. items_data = [it.model_dump() for it in body.items]
  412. count = reagent_svc.batch_stock_in(
  413. db, items=items_data, remark=body.remark,
  414. tenant_no=tenant_no, user_id=user.get("user_id"),
  415. )
  416. return success_response({"count": count}, message=f"成功入库 {count} 条")
  417. except ValueError as e:
  418. return error_response(400, str(e))
  419. @router.post("/reagents/batch-stock-out")
  420. async def reagent_batch_stock_out(
  421. body: BatchStockOutBody,
  422. user: dict = Depends(require_admin_or_super_admin),
  423. db: Session = Depends(get_db),
  424. ):
  425. try:
  426. tenant_no = _get_tenant_no(db, user)
  427. items_data = [it.model_dump() for it in body.items]
  428. count = reagent_svc.batch_stock_out(
  429. db, items=items_data,
  430. receiver=body.receiver, purpose=body.purpose, remark=body.remark,
  431. tenant_no=tenant_no, user_id=user.get("user_id"),
  432. )
  433. return success_response({"count": count}, message=f"成功出库 {count} 条")
  434. except ValueError as e:
  435. return error_response(400, str(e))
  436. @router.get("/reagents/{reagent_id}/stock-logs")
  437. async def list_reagent_stock_logs(
  438. reagent_id: str,
  439. page: int = Query(1, ge=1),
  440. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  441. changeType: Optional[str] = None,
  442. user: dict = Depends(require_admin_or_super_admin),
  443. db: Session = Depends(get_db),
  444. ):
  445. tenant_no = _get_tenant_no(db, user)
  446. items, total = reagent_stock_log_svc.list_stock_logs(
  447. db, tenant_no, page=page, page_size=pageSize,
  448. material_id=reagent_id, change_type=changeType,
  449. user_info=user,
  450. )
  451. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  452. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  453. @router.get("/reagents/stock-logs")
  454. async def list_all_reagent_stock_logs(
  455. page: int = Query(1, ge=1),
  456. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  457. keyword: Optional[str] = None,
  458. changeType: Optional[str] = None,
  459. startTime: Optional[str] = None,
  460. endTime: Optional[str] = None,
  461. user: dict = Depends(require_admin_or_super_admin),
  462. db: Session = Depends(get_db),
  463. ):
  464. tenant_no = _get_tenant_no(db, user)
  465. items, total = reagent_stock_log_svc.list_stock_logs(
  466. db, tenant_no, page=page, page_size=pageSize,
  467. keyword=keyword, change_type=changeType,
  468. start_time=startTime, end_time=endTime,
  469. user_info=user,
  470. )
  471. tp = (total + pageSize - 1) // pageSize if total > 0 else 0
  472. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  473. # ---------------------------------------------------------------------------
  474. # 3. SOP文档
  475. # ---------------------------------------------------------------------------
  476. class SopDocumentCreateBody(BaseModel):
  477. model_config = ConfigDict(populate_by_name=True)
  478. sop_code: str = Field(..., alias="sopCode")
  479. sop_name: str = Field(..., alias="sopName")
  480. version_no: Optional[str] = Field(None, alias="versionNo")
  481. sop_attachments: Optional[str] = Field(None, alias="sopAttachments")
  482. order_num: Optional[int] = Field(0, alias="orderNum")
  483. remark: Optional[str] = None
  484. class SopDocumentUpdateBody(BaseModel):
  485. model_config = ConfigDict(populate_by_name=True)
  486. sop_code: Optional[str] = Field(None, alias="sopCode")
  487. sop_name: Optional[str] = Field(None, alias="sopName")
  488. version_no: Optional[str] = Field(None, alias="versionNo")
  489. sop_attachments: Optional[str] = Field(None, alias="sopAttachments")
  490. order_num: Optional[int] = Field(None, alias="orderNum")
  491. remark: Optional[str] = None
  492. @router.get("/sop-documents")
  493. async def list_sop_documents(
  494. page: int = Query(1, ge=1),
  495. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  496. keyword: Optional[str] = None,
  497. user: dict = Depends(require_menu_code("production:sop-document:list")),
  498. db: Session = Depends(get_db),
  499. ):
  500. tenant_no = _get_tenant_no(db, user)
  501. items, total = sop_document_svc.list_sop_documents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  502. tp = sop_document_svc.total_pages(total, pageSize)
  503. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  504. @router.get("/sop-documents/all")
  505. async def list_all_sop_documents(
  506. user: dict = Depends(require_admin_or_super_admin),
  507. db: Session = Depends(get_db),
  508. ):
  509. tenant_no = _get_tenant_no(db, user)
  510. items, _ = sop_document_svc.list_sop_documents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  511. return success_response(items)
  512. @router.post("/sop-documents")
  513. async def create_sop_document(
  514. body: SopDocumentCreateBody,
  515. user: dict = Depends(require_admin_or_super_admin),
  516. db: Session = Depends(get_db),
  517. ):
  518. try:
  519. tenant_no = _get_tenant_no(db, user)
  520. obj = sop_document_svc.create_sop_document(
  521. db,
  522. sop_code=body.sop_code,
  523. sop_name=body.sop_name,
  524. version_no=body.version_no,
  525. sop_attachments=body.sop_attachments,
  526. tenant_no=tenant_no,
  527. order_num=body.order_num,
  528. remark=body.remark,
  529. user_id=user.get("user_id"),
  530. )
  531. return success_response(
  532. sop_document_svc._row_to_dict(obj, db, tenant_no),
  533. message="创建成功",
  534. )
  535. except ValueError as e:
  536. return error_response(400, str(e))
  537. @router.put("/sop-documents/{sop_id}")
  538. async def update_sop_document(
  539. sop_id: str,
  540. body: SopDocumentUpdateBody,
  541. user: dict = Depends(require_admin_or_super_admin),
  542. db: Session = Depends(get_db),
  543. ):
  544. try:
  545. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  546. obj = sop_document_svc.update_sop_document(
  547. db,
  548. sop_id,
  549. sop_code=payload.get("sop_code"),
  550. sop_name=payload.get("sop_name"),
  551. version_no=payload.get("version_no"),
  552. sop_attachments=payload.get("sop_attachments"),
  553. order_num=payload.get("order_num"),
  554. remark=payload.get("remark"),
  555. user_id=user.get("user_id"),
  556. )
  557. tenant_no = _get_tenant_no(db, user)
  558. return success_response(
  559. sop_document_svc._row_to_dict(obj, db, tenant_no),
  560. message="更新成功",
  561. )
  562. except ValueError as e:
  563. return error_response(400, str(e))
  564. @router.delete("/sop-documents/{sop_id}")
  565. async def delete_sop_document(
  566. sop_id: str,
  567. _user: dict = Depends(require_admin_or_super_admin),
  568. db: Session = Depends(get_db),
  569. ):
  570. try:
  571. sop_document_svc.soft_delete_sop_document(db, sop_id)
  572. return success_response(None, message="删除成功")
  573. except ValueError as e:
  574. return error_response(400, str(e))
  575. # ---------------------------------------------------------------------------
  576. # 4. 工艺工序分类
  577. # ---------------------------------------------------------------------------
  578. class ProcessCategoryCreateBody(BaseModel):
  579. model_config = ConfigDict(populate_by_name=True)
  580. category_code: str = Field(..., alias="categoryCode")
  581. category_name: str = Field(..., alias="categoryName")
  582. order_num: Optional[int] = Field(0, alias="orderNum")
  583. remark: Optional[str] = None
  584. class ProcessCategoryUpdateBody(BaseModel):
  585. model_config = ConfigDict(populate_by_name=True)
  586. category_code: Optional[str] = Field(None, alias="categoryCode")
  587. category_name: Optional[str] = Field(None, alias="categoryName")
  588. order_num: Optional[int] = Field(None, alias="orderNum")
  589. remark: Optional[str] = None
  590. @router.get("/process-categories")
  591. async def list_process_categories(
  592. page: int = Query(1, ge=1),
  593. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  594. keyword: Optional[str] = None,
  595. user: dict = Depends(require_menu_code("production:process-category:list")),
  596. db: Session = Depends(get_db),
  597. ):
  598. tenant_no = _get_tenant_no(db, user)
  599. items, total = process_category_svc.list_process_categories(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  600. tp = process_category_svc.total_pages(total, pageSize)
  601. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  602. @router.get("/process-categories/all")
  603. async def list_all_process_categories(
  604. user: dict = Depends(require_admin_or_super_admin),
  605. db: Session = Depends(get_db),
  606. ):
  607. tenant_no = _get_tenant_no(db, user)
  608. items, _ = process_category_svc.list_process_categories(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  609. return success_response(items)
  610. @router.post("/process-categories")
  611. async def create_process_category(
  612. body: ProcessCategoryCreateBody,
  613. user: dict = Depends(require_admin_or_super_admin),
  614. db: Session = Depends(get_db),
  615. ):
  616. try:
  617. tenant_no = _get_tenant_no(db, user)
  618. obj = process_category_svc.create_process_category(
  619. db,
  620. category_code=body.category_code,
  621. category_name=body.category_name,
  622. tenant_no=tenant_no,
  623. order_num=body.order_num,
  624. remark=body.remark,
  625. user_id=user.get("user_id"),
  626. )
  627. return success_response(process_category_svc._row_to_dict(obj), message="创建成功")
  628. except ValueError as e:
  629. return error_response(400, str(e))
  630. @router.put("/process-categories/{category_id}")
  631. async def update_process_category(
  632. category_id: str,
  633. body: ProcessCategoryUpdateBody,
  634. user: dict = Depends(require_admin_or_super_admin),
  635. db: Session = Depends(get_db),
  636. ):
  637. try:
  638. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  639. obj = process_category_svc.update_process_category(
  640. db,
  641. category_id,
  642. category_code=payload.get("category_code"),
  643. category_name=payload.get("category_name"),
  644. order_num=payload.get("order_num"),
  645. remark=payload.get("remark"),
  646. user_id=user.get("user_id"),
  647. )
  648. return success_response(process_category_svc._row_to_dict(obj), message="更新成功")
  649. except ValueError as e:
  650. return error_response(400, str(e))
  651. @router.delete("/process-categories/{category_id}")
  652. async def delete_process_category(
  653. category_id: str,
  654. _user: dict = Depends(require_admin_or_super_admin),
  655. db: Session = Depends(get_db),
  656. ):
  657. try:
  658. process_category_svc.soft_delete_process_category(db, category_id)
  659. return success_response(None, message="删除成功")
  660. except ValueError as e:
  661. return error_response(400, str(e))
  662. # ---------------------------------------------------------------------------
  663. # 设备管理
  664. # ---------------------------------------------------------------------------
  665. class EquipmentCreateBody(BaseModel):
  666. model_config = ConfigDict(populate_by_name=True)
  667. equipment_code: str = Field(..., alias="equipmentCode")
  668. equipment_name: str = Field(..., alias="equipmentName")
  669. equipment_model: Optional[str] = Field(None, alias="equipmentModel")
  670. manufacturer: Optional[str] = None
  671. purchase_date: Optional[str] = Field(None, alias="purchaseDate")
  672. purchase_amount: Optional[float] = Field(None, alias="purchaseAmount")
  673. location: Optional[str] = None
  674. serial_no: Optional[str] = Field(None, alias="serialNo")
  675. status: Optional[str] = "1"
  676. order_num: Optional[int] = Field(0, alias="orderNum")
  677. remark: Optional[str] = None
  678. class EquipmentUpdateBody(BaseModel):
  679. model_config = ConfigDict(populate_by_name=True)
  680. equipment_code: Optional[str] = Field(None, alias="equipmentCode")
  681. equipment_name: Optional[str] = Field(None, alias="equipmentName")
  682. equipment_model: Optional[str] = Field(None, alias="equipmentModel")
  683. manufacturer: Optional[str] = None
  684. purchase_date: Optional[str] = Field(None, alias="purchaseDate")
  685. purchase_amount: Optional[float] = Field(None, alias="purchaseAmount")
  686. location: Optional[str] = None
  687. serial_no: Optional[str] = Field(None, alias="serialNo")
  688. status: Optional[str] = None
  689. order_num: Optional[int] = Field(None, alias="orderNum")
  690. remark: Optional[str] = None
  691. @router.get("/equipments")
  692. async def list_equipments(
  693. page: int = Query(1, ge=1),
  694. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  695. keyword: Optional[str] = None,
  696. status: Optional[str] = None,
  697. user: dict = Depends(require_menu_code("production:equipment:list")),
  698. db: Session = Depends(get_db),
  699. ):
  700. tenant_no = _get_tenant_no(db, user)
  701. items, total = equipment_svc.list_equipments(
  702. db,
  703. page=page,
  704. page_size=pageSize,
  705. keyword=keyword,
  706. status=status,
  707. tenant_no=tenant_no,
  708. user_info=user,
  709. )
  710. tp = equipment_svc.total_pages(total, pageSize)
  711. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  712. @router.get("/equipments/all")
  713. async def list_all_equipments(
  714. user: dict = Depends(require_admin_or_super_admin),
  715. db: Session = Depends(get_db),
  716. ):
  717. tenant_no = _get_tenant_no(db, user)
  718. items, _ = equipment_svc.list_equipments(
  719. db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user
  720. )
  721. return success_response(items)
  722. @router.post("/equipments")
  723. async def create_equipment(
  724. body: EquipmentCreateBody,
  725. user: dict = Depends(require_admin_or_super_admin),
  726. db: Session = Depends(get_db),
  727. ):
  728. try:
  729. tenant_no = _get_tenant_no(db, user)
  730. obj = equipment_svc.create_equipment(
  731. db,
  732. equipment_code=body.equipment_code,
  733. equipment_name=body.equipment_name,
  734. equipment_model=body.equipment_model,
  735. manufacturer=body.manufacturer,
  736. purchase_date=body.purchase_date,
  737. purchase_amount=body.purchase_amount,
  738. location=body.location,
  739. serial_no=body.serial_no,
  740. status=body.status,
  741. tenant_no=tenant_no,
  742. order_num=body.order_num,
  743. remark=body.remark,
  744. user_id=user.get("user_id"),
  745. )
  746. return success_response(equipment_svc._row_to_dict(obj), message="创建成功")
  747. except ValueError as e:
  748. return error_response(400, str(e))
  749. @router.put("/equipments/{equipment_id}")
  750. async def update_equipment(
  751. equipment_id: str,
  752. body: EquipmentUpdateBody,
  753. user: dict = Depends(require_admin_or_super_admin),
  754. db: Session = Depends(get_db),
  755. ):
  756. try:
  757. payload: dict[str, Any] = body.model_dump(by_alias=False)
  758. obj = equipment_svc.update_equipment(
  759. db,
  760. equipment_id,
  761. equipment_code=payload.get("equipment_code"),
  762. equipment_name=payload.get("equipment_name"),
  763. equipment_model=payload.get("equipment_model"),
  764. manufacturer=payload.get("manufacturer"),
  765. purchase_date=payload.get("purchase_date"),
  766. purchase_amount=payload.get("purchase_amount"),
  767. location=payload.get("location"),
  768. serial_no=payload.get("serial_no"),
  769. status=payload.get("status"),
  770. order_num=payload.get("order_num"),
  771. remark=payload.get("remark"),
  772. user_id=user.get("user_id"),
  773. )
  774. return success_response(equipment_svc._row_to_dict(obj), message="更新成功")
  775. except ValueError as e:
  776. return error_response(400, str(e))
  777. @router.delete("/equipments/{equipment_id}")
  778. async def delete_equipment(
  779. equipment_id: str,
  780. _user: dict = Depends(require_admin_or_super_admin),
  781. db: Session = Depends(get_db),
  782. ):
  783. try:
  784. equipment_svc.soft_delete_equipment(db, equipment_id)
  785. return success_response(None, message="删除成功")
  786. except ValueError as e:
  787. return error_response(400, str(e))
  788. # ---------------------------------------------------------------------------
  789. # 5. 工艺产出物类型
  790. # ---------------------------------------------------------------------------
  791. class OutputTypeCreateBody(BaseModel):
  792. model_config = ConfigDict(populate_by_name=True)
  793. type_code: str = Field(..., alias="typeCode")
  794. type_name: str = Field(..., alias="typeName")
  795. order_num: Optional[int] = Field(0, alias="orderNum")
  796. remark: Optional[str] = None
  797. class OutputTypeUpdateBody(BaseModel):
  798. model_config = ConfigDict(populate_by_name=True)
  799. type_code: Optional[str] = Field(None, alias="typeCode")
  800. type_name: Optional[str] = Field(None, alias="typeName")
  801. order_num: Optional[int] = Field(None, alias="orderNum")
  802. remark: Optional[str] = None
  803. @router.get("/output-types")
  804. async def list_output_types(
  805. page: int = Query(1, ge=1),
  806. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  807. keyword: Optional[str] = None,
  808. user: dict = Depends(require_menu_code("production:output-type:list")),
  809. db: Session = Depends(get_db),
  810. ):
  811. tenant_no = _get_tenant_no(db, user)
  812. items, total = output_type_svc.list_output_types(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  813. tp = output_type_svc.total_pages(total, pageSize)
  814. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  815. @router.get("/output-types/all")
  816. async def list_all_output_types(
  817. user: dict = Depends(require_admin_or_super_admin),
  818. db: Session = Depends(get_db),
  819. ):
  820. tenant_no = _get_tenant_no(db, user)
  821. items, _ = output_type_svc.list_output_types(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
  822. return success_response(items)
  823. @router.post("/output-types")
  824. async def create_output_type(
  825. body: OutputTypeCreateBody,
  826. user: dict = Depends(require_admin_or_super_admin),
  827. db: Session = Depends(get_db),
  828. ):
  829. try:
  830. tenant_no = _get_tenant_no(db, user)
  831. obj = output_type_svc.create_output_type(
  832. db,
  833. type_code=body.type_code,
  834. type_name=body.type_name,
  835. tenant_no=tenant_no,
  836. order_num=body.order_num,
  837. remark=body.remark,
  838. user_id=user.get("user_id"),
  839. )
  840. return success_response(output_type_svc._row_to_dict(obj), message="创建成功")
  841. except ValueError as e:
  842. return error_response(400, str(e))
  843. @router.put("/output-types/{type_id}")
  844. async def update_output_type(
  845. type_id: str,
  846. body: OutputTypeUpdateBody,
  847. user: dict = Depends(require_admin_or_super_admin),
  848. db: Session = Depends(get_db),
  849. ):
  850. try:
  851. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  852. obj = output_type_svc.update_output_type(
  853. db,
  854. type_id,
  855. type_code=payload.get("type_code"),
  856. type_name=payload.get("type_name"),
  857. order_num=payload.get("order_num"),
  858. remark=payload.get("remark"),
  859. user_id=user.get("user_id"),
  860. )
  861. return success_response(output_type_svc._row_to_dict(obj), message="更新成功")
  862. except ValueError as e:
  863. return error_response(400, str(e))
  864. @router.delete("/output-types/{type_id}")
  865. async def delete_output_type(
  866. type_id: str,
  867. _user: dict = Depends(require_admin_or_super_admin),
  868. db: Session = Depends(get_db),
  869. ):
  870. try:
  871. output_type_svc.soft_delete_output_type(db, type_id)
  872. return success_response(None, message="删除成功")
  873. except ValueError as e:
  874. return error_response(400, str(e))
  875. # ---------------------------------------------------------------------------
  876. # 5b. 生产产品类型(生产专用,解耦中试 biz_cell_product_type)
  877. # ---------------------------------------------------------------------------
  878. class ProductionProductTypeCreateBody(BaseModel):
  879. model_config = ConfigDict(populate_by_name=True)
  880. product_type_code: str = Field(..., alias="productTypeCode")
  881. product_type_name: str = Field(..., alias="productTypeName")
  882. order_num: Optional[int] = Field(0, alias="orderNum")
  883. remark: Optional[str] = None
  884. class ProductionProductTypeUpdateBody(BaseModel):
  885. model_config = ConfigDict(populate_by_name=True)
  886. product_type_code: Optional[str] = Field(None, alias="productTypeCode")
  887. product_type_name: Optional[str] = Field(None, alias="productTypeName")
  888. order_num: Optional[int] = Field(None, alias="orderNum")
  889. remark: Optional[str] = None
  890. @router.get("/production-product-types")
  891. async def list_production_product_types(
  892. page: int = Query(1, ge=1),
  893. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  894. keyword: Optional[str] = None,
  895. user: dict = Depends(require_admin_or_super_admin),
  896. db: Session = Depends(get_db),
  897. ):
  898. tenant_no = _get_tenant_no(db, user)
  899. items, total = production_product_type_svc.list_production_product_types(
  900. db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
  901. )
  902. tp = production_product_type_svc.total_pages(total, pageSize)
  903. return success_response(
  904. {"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}
  905. )
  906. @router.post("/production-product-types")
  907. async def create_production_product_type(
  908. body: ProductionProductTypeCreateBody,
  909. user: dict = Depends(require_admin_or_super_admin),
  910. db: Session = Depends(get_db),
  911. ):
  912. try:
  913. tenant_no = _get_tenant_no(db, user)
  914. obj = production_product_type_svc.create_production_product_type(
  915. db,
  916. product_type_code=body.product_type_code,
  917. product_type_name=body.product_type_name,
  918. tenant_no=tenant_no,
  919. order_num=body.order_num,
  920. remark=body.remark,
  921. user_id=user.get("user_id"),
  922. )
  923. return success_response(production_product_type_svc._row_to_dict(obj), message="创建成功")
  924. except ValueError as e:
  925. return error_response(400, str(e))
  926. @router.put("/production-product-types/{row_id}")
  927. async def update_production_product_type(
  928. row_id: str,
  929. body: ProductionProductTypeUpdateBody,
  930. user: dict = Depends(require_admin_or_super_admin),
  931. db: Session = Depends(get_db),
  932. ):
  933. try:
  934. tenant_no = _get_tenant_no(db, user)
  935. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  936. obj = production_product_type_svc.update_production_product_type(
  937. db,
  938. row_id,
  939. product_type_code=payload.get("product_type_code"),
  940. product_type_name=payload.get("product_type_name"),
  941. order_num=payload.get("order_num"),
  942. remark=payload.get("remark"),
  943. user_id=user.get("user_id"),
  944. tenant_no=tenant_no,
  945. )
  946. return success_response(production_product_type_svc._row_to_dict(obj), message="更新成功")
  947. except ValueError as e:
  948. return error_response(400, str(e))
  949. @router.delete("/production-product-types/{row_id}")
  950. async def delete_production_product_type(
  951. row_id: str,
  952. user: dict = Depends(require_admin_or_super_admin),
  953. db: Session = Depends(get_db),
  954. ):
  955. try:
  956. tenant_no = _get_tenant_no(db, user)
  957. production_product_type_svc.soft_delete_production_product_type(
  958. db, row_id, tenant_no=tenant_no
  959. )
  960. return success_response(None, message="删除成功")
  961. except ValueError as e:
  962. return error_response(400, str(e))
  963. # ---------------------------------------------------------------------------
  964. # 6. 耗材包(主 + 子项)
  965. # ---------------------------------------------------------------------------
  966. class PackItemInput(BaseModel):
  967. model_config = ConfigDict(populate_by_name=True)
  968. consumable_code: Optional[str] = Field(None, alias="consumableCode")
  969. consumable_name: Optional[str] = Field(None, alias="consumableName")
  970. spec_model: Optional[str] = Field(None, alias="specModel")
  971. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  972. unit: Optional[str] = None
  973. remark: Optional[str] = None
  974. class ConsumablePackCreateBody(BaseModel):
  975. model_config = ConfigDict(populate_by_name=True)
  976. pack_code: str = Field(..., alias="packCode")
  977. pack_name: str = Field(..., alias="packName")
  978. usage_scene: Optional[str] = Field(None, alias="usageScene")
  979. pack_type: Optional[str] = Field(None, alias="packType")
  980. status: Optional[int] = 1
  981. order_num: Optional[int] = Field(0, alias="orderNum")
  982. remark: Optional[str] = None
  983. items: List[PackItemInput] = []
  984. class ConsumablePackUpdateBody(BaseModel):
  985. model_config = ConfigDict(populate_by_name=True)
  986. pack_code: Optional[str] = Field(None, alias="packCode")
  987. pack_name: Optional[str] = Field(None, alias="packName")
  988. usage_scene: Optional[str] = Field(None, alias="usageScene")
  989. pack_type: Optional[str] = Field(None, alias="packType")
  990. status: Optional[int] = None
  991. order_num: Optional[int] = Field(None, alias="orderNum")
  992. remark: Optional[str] = None
  993. items: Optional[List[PackItemInput]] = None
  994. @router.get("/consumable-packs")
  995. async def list_consumable_packs(
  996. page: int = Query(1, ge=1),
  997. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  998. keyword: Optional[str] = None,
  999. user: dict = Depends(require_menu_code("production:consumable-pack:list")),
  1000. db: Session = Depends(get_db),
  1001. ):
  1002. tenant_no = _get_tenant_no(db, user)
  1003. items, total = consumable_pack_svc.list_consumable_packs(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  1004. tp = consumable_pack_svc.total_pages(total, pageSize)
  1005. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1006. @router.get("/consumable-packs/all")
  1007. async def list_all_consumable_packs(
  1008. user: dict = Depends(require_admin_or_super_admin),
  1009. db: Session = Depends(get_db),
  1010. ):
  1011. tenant_no = _get_tenant_no(db, user)
  1012. items = consumable_pack_svc.list_all_packs(db, tenant_no, user_info=user)
  1013. return success_response(items)
  1014. @router.get("/consumable-packs/{pack_id}")
  1015. async def get_consumable_pack_detail(
  1016. pack_id: str,
  1017. user: dict = Depends(require_admin_or_super_admin),
  1018. db: Session = Depends(get_db),
  1019. ):
  1020. detail = consumable_pack_svc.get_pack_detail(db, pack_id)
  1021. if not detail:
  1022. return error_response(404, "耗材包不存在")
  1023. return success_response(detail)
  1024. @router.post("/consumable-packs")
  1025. async def create_consumable_pack(
  1026. body: ConsumablePackCreateBody,
  1027. user: dict = Depends(require_admin_or_super_admin),
  1028. db: Session = Depends(get_db),
  1029. ):
  1030. try:
  1031. tenant_no = _get_tenant_no(db, user)
  1032. items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items else None
  1033. obj = consumable_pack_svc.create_consumable_pack(
  1034. db,
  1035. pack_code=body.pack_code,
  1036. pack_name=body.pack_name,
  1037. usage_scene=body.usage_scene,
  1038. pack_type=body.pack_type,
  1039. status=body.status,
  1040. items=items_data,
  1041. tenant_no=tenant_no,
  1042. order_num=body.order_num,
  1043. remark=body.remark,
  1044. user_id=user.get("user_id"),
  1045. )
  1046. return success_response(consumable_pack_svc._row_to_dict(obj), message="创建成功")
  1047. except ValueError as e:
  1048. return error_response(400, str(e))
  1049. @router.put("/consumable-packs/{pack_id}")
  1050. async def update_consumable_pack(
  1051. pack_id: str,
  1052. body: ConsumablePackUpdateBody,
  1053. user: dict = Depends(require_admin_or_super_admin),
  1054. db: Session = Depends(get_db),
  1055. ):
  1056. try:
  1057. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1058. items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items is not None else None
  1059. obj = consumable_pack_svc.update_consumable_pack(
  1060. db,
  1061. pack_id,
  1062. pack_code=payload.get("pack_code"),
  1063. pack_name=payload.get("pack_name"),
  1064. usage_scene=payload.get("usage_scene"),
  1065. pack_type=payload.get("pack_type"),
  1066. status=payload.get("status"),
  1067. items=items_data,
  1068. order_num=payload.get("order_num"),
  1069. remark=payload.get("remark"),
  1070. user_id=user.get("user_id"),
  1071. )
  1072. return success_response(consumable_pack_svc._row_to_dict(obj), message="更新成功")
  1073. except ValueError as e:
  1074. return error_response(400, str(e))
  1075. @router.delete("/consumable-packs/{pack_id}")
  1076. async def delete_consumable_pack(
  1077. pack_id: str,
  1078. _user: dict = Depends(require_admin_or_super_admin),
  1079. db: Session = Depends(get_db),
  1080. ):
  1081. try:
  1082. consumable_pack_svc.soft_delete_consumable_pack(db, pack_id)
  1083. return success_response(None, message="删除成功")
  1084. except ValueError as e:
  1085. return error_response(400, str(e))
  1086. # ---------------------------------------------------------------------------
  1087. # 7. 工艺库(主 + 3 个子表:试剂、耗材、产出物)
  1088. # ---------------------------------------------------------------------------
  1089. class ProcessReagentInput(BaseModel):
  1090. model_config = ConfigDict(populate_by_name=True)
  1091. reagent_code: Optional[str] = Field(None, alias="reagentCode")
  1092. reagent_name: Optional[str] = Field(None, alias="reagentName")
  1093. spec: Optional[str] = None
  1094. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  1095. unit: Optional[str] = None
  1096. is_key_material: Optional[str] = Field("0", alias="isKeyMaterial")
  1097. remark: Optional[str] = None
  1098. class ProcessConsumableInput(BaseModel):
  1099. model_config = ConfigDict(populate_by_name=True)
  1100. consumable_code: Optional[str] = Field(None, alias="consumableCode")
  1101. consumable_name: Optional[str] = Field(None, alias="consumableName")
  1102. spec_model: Optional[str] = Field(None, alias="specModel")
  1103. standard_usage: Optional[str] = Field(None, alias="standardUsage")
  1104. unit: Optional[str] = None
  1105. is_required: Optional[str] = Field("1", alias="isRequired")
  1106. remark: Optional[str] = None
  1107. class ProcessOutputInput(BaseModel):
  1108. model_config = ConfigDict(populate_by_name=True)
  1109. output_type_id: Optional[str] = Field(None, alias="outputTypeId")
  1110. output_name: Optional[str] = Field(None, alias="outputName")
  1111. output_barcode: Optional[str] = Field(None, alias="outputBarcode")
  1112. remark: Optional[str] = None
  1113. class ProcessDataCollectionInput(BaseModel):
  1114. model_config = ConfigDict(populate_by_name=True)
  1115. collection_name: str = Field(..., alias="collectionName")
  1116. unit: Optional[str] = None
  1117. order_num: Optional[int] = Field(0, alias="orderNum")
  1118. remark: Optional[str] = None
  1119. class ProcessCreateBody(BaseModel):
  1120. model_config = ConfigDict(populate_by_name=True)
  1121. process_code: str = Field(..., alias="processCode")
  1122. category_id: Optional[str] = Field(None, alias="categoryId")
  1123. process_name: str = Field(..., alias="processName")
  1124. description: Optional[str] = None
  1125. equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds")
  1126. estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
  1127. sop_ids: Optional[str] = Field(None, alias="sopIds")
  1128. order_num: Optional[int] = Field(0, alias="orderNum")
  1129. remark: Optional[str] = None
  1130. reagents: List[ProcessReagentInput] = []
  1131. consumables: List[ProcessConsumableInput] = []
  1132. outputs: List[ProcessOutputInput] = []
  1133. data_collections: List[ProcessDataCollectionInput] = Field([], alias="dataCollections")
  1134. class ProcessUpdateBody(BaseModel):
  1135. model_config = ConfigDict(populate_by_name=True)
  1136. process_code: Optional[str] = Field(None, alias="processCode")
  1137. category_id: Optional[str] = Field(None, alias="categoryId")
  1138. process_name: Optional[str] = Field(None, alias="processName")
  1139. description: Optional[str] = None
  1140. equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds")
  1141. estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
  1142. sop_ids: Optional[str] = Field(None, alias="sopIds")
  1143. order_num: Optional[int] = Field(None, alias="orderNum")
  1144. remark: Optional[str] = None
  1145. reagents: Optional[List[ProcessReagentInput]] = None
  1146. consumables: Optional[List[ProcessConsumableInput]] = None
  1147. outputs: Optional[List[ProcessOutputInput]] = None
  1148. data_collections: Optional[List[ProcessDataCollectionInput]] = Field(None, alias="dataCollections")
  1149. @router.get("/processes")
  1150. async def list_processes(
  1151. page: int = Query(1, ge=1),
  1152. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1153. keyword: Optional[str] = None,
  1154. user: dict = Depends(require_menu_code("production:process:list")),
  1155. db: Session = Depends(get_db),
  1156. ):
  1157. tenant_no = _get_tenant_no(db, user)
  1158. items, total = process_svc.list_processes(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  1159. tp = process_svc.total_pages(total, pageSize)
  1160. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1161. @router.get("/processes/all")
  1162. async def list_all_processes(
  1163. user: dict = Depends(require_admin_or_super_admin),
  1164. db: Session = Depends(get_db),
  1165. ):
  1166. tenant_no = _get_tenant_no(db, user)
  1167. items = process_svc.list_all_processes(db, tenant_no, user_info=user)
  1168. return success_response(items)
  1169. @router.get("/processes/{process_id}")
  1170. async def get_process_detail(
  1171. process_id: str,
  1172. user: dict = Depends(require_admin_or_super_admin),
  1173. db: Session = Depends(get_db),
  1174. ):
  1175. detail = process_svc.get_process_detail(db, process_id)
  1176. if not detail:
  1177. return error_response(404, "工艺不存在")
  1178. return success_response(detail)
  1179. @router.post("/processes")
  1180. async def create_process(
  1181. body: ProcessCreateBody,
  1182. user: dict = Depends(require_admin_or_super_admin),
  1183. db: Session = Depends(get_db),
  1184. ):
  1185. try:
  1186. tenant_no = _get_tenant_no(db, user)
  1187. reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents else None
  1188. consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables else None
  1189. outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs else None
  1190. data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections else None
  1191. # Convert string fields to appropriate types for sub-records
  1192. for rd in (reagents_data or []):
  1193. if "is_key_material" in rd and isinstance(rd["is_key_material"], str):
  1194. rd["is_key_material"] = int(rd["is_key_material"])
  1195. for cd in (consumables_data or []):
  1196. if "is_required" in cd and isinstance(cd["is_required"], str):
  1197. cd["is_required"] = int(cd["is_required"])
  1198. obj = process_svc.create_process(
  1199. db,
  1200. process_code=body.process_code,
  1201. category_id=body.category_id,
  1202. process_name=body.process_name,
  1203. description=body.description,
  1204. equipment_ids=body.equipment_ids,
  1205. estimated_duration=body.estimated_duration,
  1206. sop_ids=body.sop_ids,
  1207. reagents=reagents_data,
  1208. consumables=consumables_data,
  1209. outputs=outputs_data,
  1210. data_collections=data_collections_data,
  1211. tenant_no=tenant_no,
  1212. order_num=body.order_num,
  1213. remark=body.remark,
  1214. user_id=user.get("user_id"),
  1215. )
  1216. return success_response(process_svc._row_to_dict(obj), message="创建成功")
  1217. except ValueError as e:
  1218. return error_response(400, str(e))
  1219. @router.put("/processes/{process_id}")
  1220. async def update_process(
  1221. process_id: str,
  1222. body: ProcessUpdateBody,
  1223. user: dict = Depends(require_admin_or_super_admin),
  1224. db: Session = Depends(get_db),
  1225. ):
  1226. try:
  1227. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1228. reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents is not None else None
  1229. consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables is not None else None
  1230. outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs is not None else None
  1231. data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections is not None else None
  1232. # Convert string fields to appropriate types for sub-records
  1233. for rd in (reagents_data or []):
  1234. if "is_key_material" in rd and isinstance(rd["is_key_material"], str):
  1235. rd["is_key_material"] = int(rd["is_key_material"])
  1236. for cd in (consumables_data or []):
  1237. if "is_required" in cd and isinstance(cd["is_required"], str):
  1238. cd["is_required"] = int(cd["is_required"])
  1239. update_kwargs: dict[str, Any] = {
  1240. "process_code": payload.get("process_code"),
  1241. "category_id": payload.get("category_id"),
  1242. "process_name": payload.get("process_name"),
  1243. "description": payload.get("description"),
  1244. "estimated_duration": payload.get("estimated_duration"),
  1245. "sop_ids": payload.get("sop_ids"),
  1246. "reagents": reagents_data,
  1247. "consumables": consumables_data,
  1248. "outputs": outputs_data,
  1249. "data_collections": data_collections_data,
  1250. "order_num": payload.get("order_num"),
  1251. "remark": payload.get("remark"),
  1252. "user_id": user.get("user_id"),
  1253. }
  1254. if "equipment_ids" in payload:
  1255. update_kwargs["equipment_ids"] = payload.get("equipment_ids")
  1256. obj = process_svc.update_process(db, process_id, **update_kwargs)
  1257. return success_response(process_svc._row_to_dict(obj), message="更新成功")
  1258. except ValueError as e:
  1259. return error_response(400, str(e))
  1260. @router.delete("/processes/{process_id}")
  1261. async def delete_process(
  1262. process_id: str,
  1263. _user: dict = Depends(require_admin_or_super_admin),
  1264. db: Session = Depends(get_db),
  1265. ):
  1266. try:
  1267. process_svc.soft_delete_process(db, process_id)
  1268. return success_response(None, message="删除成功")
  1269. except ValueError as e:
  1270. return error_response(400, str(e))
  1271. # ---------------------------------------------------------------------------
  1272. # 8. 工艺路线管理
  1273. # ---------------------------------------------------------------------------
  1274. @router.get("/process-routes")
  1275. async def list_process_routes(
  1276. page: int = Query(1, ge=1),
  1277. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1278. routeName: Optional[str] = None,
  1279. routeCode: Optional[str] = None,
  1280. productTypeId: Optional[str] = None,
  1281. user: dict = Depends(require_menu_code("production:process-route:list")),
  1282. db: Session = Depends(get_db),
  1283. ):
  1284. tenant_no = _get_tenant_no(db, user)
  1285. items, total = route_svc.list_routes(
  1286. db, page=page, page_size=pageSize,
  1287. route_name=routeName, route_code=routeCode,
  1288. product_type_id=productTypeId, tenant_no=tenant_no,
  1289. user_info=user,
  1290. )
  1291. tp = route_svc.total_pages(total, pageSize)
  1292. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1293. @router.get("/process-routes/{route_id}")
  1294. async def get_process_route_detail(
  1295. route_id: str,
  1296. _user: dict = Depends(require_admin_or_super_admin),
  1297. db: Session = Depends(get_db),
  1298. ):
  1299. detail = route_svc.get_route_detail(db, route_id)
  1300. if not detail:
  1301. return error_response(404, "路线不存在")
  1302. return success_response(detail)
  1303. @router.post("/process-routes")
  1304. async def create_process_route(
  1305. body: ProcessRouteCreateBody,
  1306. user: dict = Depends(require_admin_or_super_admin),
  1307. db: Session = Depends(get_db),
  1308. ):
  1309. try:
  1310. tenant_no = _get_tenant_no(db, user)
  1311. obj = route_svc.create_route(
  1312. db,
  1313. route_code=body.route_code,
  1314. route_name=body.route_name,
  1315. product_type_id=body.product_type_id,
  1316. version=body.version,
  1317. remark=body.remark,
  1318. tenant_no=tenant_no,
  1319. user_id=user.get("user_id"),
  1320. )
  1321. return success_response(route_svc._route_row_to_dict(obj), message="创建成功")
  1322. except ValueError as e:
  1323. return error_response(400, str(e))
  1324. @router.put("/process-routes/{route_id}")
  1325. async def update_process_route(
  1326. route_id: str,
  1327. body: ProcessRouteUpdateBody,
  1328. user: dict = Depends(require_admin_or_super_admin),
  1329. db: Session = Depends(get_db),
  1330. ):
  1331. try:
  1332. payload = body.model_dump(exclude_unset=True, by_alias=False)
  1333. update_kwargs = {
  1334. "user_id": user.get("user_id"),
  1335. "route_code": payload.get("route_code"),
  1336. "route_name": payload.get("route_name"),
  1337. "product_type_id": payload.get("product_type_id"),
  1338. "version": payload.get("version"),
  1339. "remark": payload.get("remark"),
  1340. }
  1341. if "equipment_ids" in payload:
  1342. update_kwargs["equipment_ids"] = payload.get("equipment_ids")
  1343. obj = route_svc.update_route(db, route_id, **update_kwargs)
  1344. return success_response(route_svc._route_row_to_dict(obj), message="更新成功")
  1345. except ValueError as e:
  1346. return error_response(400, str(e))
  1347. @router.put("/process-routes/set-current")
  1348. async def set_current_process_route(
  1349. body: SetCurrentBody,
  1350. user: dict = Depends(require_admin_or_super_admin),
  1351. db: Session = Depends(get_db),
  1352. ):
  1353. try:
  1354. tenant_no = _get_tenant_no(db, user)
  1355. obj = route_svc.set_current_route(db, body.route_id, tenant_no)
  1356. return success_response(route_svc._route_row_to_dict(obj), message="设置成功")
  1357. except ValueError as e:
  1358. return error_response(400, str(e))
  1359. @router.delete("/process-routes/{route_id}")
  1360. async def delete_process_route(
  1361. route_id: str,
  1362. _user: dict = Depends(require_admin_or_super_admin),
  1363. db: Session = Depends(get_db),
  1364. ):
  1365. try:
  1366. route_svc.soft_delete_route(db, route_id)
  1367. return success_response(None, message="删除成功")
  1368. except ValueError as e:
  1369. return error_response(400, str(e))
  1370. # ----- 步骤操作 -----
  1371. @router.post("/process-routes/step/add")
  1372. async def add_route_step(
  1373. body: StepAddBody,
  1374. user: dict = Depends(require_admin_or_super_admin),
  1375. db: Session = Depends(get_db),
  1376. ):
  1377. try:
  1378. tenant_no = _get_tenant_no(db, user)
  1379. step_data = route_svc.add_step(
  1380. db,
  1381. route_id=body.route_id,
  1382. process_id=body.process_id,
  1383. step_order=body.step_order,
  1384. tenant_no=tenant_no,
  1385. user_id=user.get("user_id"),
  1386. )
  1387. return success_response(step_data, message="添加成功")
  1388. except ValueError as e:
  1389. return error_response(400, str(e))
  1390. @router.post("/process-routes/step/copy")
  1391. async def copy_route_step(
  1392. body: StepCopyBody,
  1393. user: dict = Depends(require_admin_or_super_admin),
  1394. db: Session = Depends(get_db),
  1395. ):
  1396. try:
  1397. tenant_no = _get_tenant_no(db, user)
  1398. step_data = route_svc.copy_step(
  1399. db,
  1400. step_id=body.step_id,
  1401. tenant_no=tenant_no,
  1402. user_id=user.get("user_id"),
  1403. )
  1404. return success_response(step_data, message="复制成功")
  1405. except ValueError as e:
  1406. return error_response(400, str(e))
  1407. @router.put("/process-routes/step/update")
  1408. async def update_route_step(
  1409. body: StepUpdateBody,
  1410. user: dict = Depends(require_admin_or_super_admin),
  1411. db: Session = Depends(get_db),
  1412. ):
  1413. try:
  1414. payload = body.model_dump(exclude_unset=True, by_alias=False)
  1415. payload.pop("step_id", None)
  1416. obj = route_svc.update_step(db, body.step_id, user_id=user.get("user_id"), **payload)
  1417. return success_response(route_svc._step_row_to_dict(obj), message="更新成功")
  1418. except ValueError as e:
  1419. return error_response(400, str(e))
  1420. @router.put("/process-routes/step/reorder")
  1421. async def reorder_route_steps(
  1422. body: StepReorderBody,
  1423. _user: dict = Depends(require_admin_or_super_admin),
  1424. db: Session = Depends(get_db),
  1425. ):
  1426. try:
  1427. route_svc.reorder_steps(db, body.route_id, body.step_ids)
  1428. return success_response(None, message="排序成功")
  1429. except ValueError as e:
  1430. return error_response(400, str(e))
  1431. @router.delete("/process-routes/step/{step_id}")
  1432. async def delete_route_step(
  1433. step_id: str,
  1434. _user: dict = Depends(require_admin_or_super_admin),
  1435. db: Session = Depends(get_db),
  1436. ):
  1437. try:
  1438. route_svc.soft_delete_step(db, step_id)
  1439. return success_response(None, message="删除成功")
  1440. except ValueError as e:
  1441. return error_response(400, str(e))
  1442. # ----- 步骤子表 -----
  1443. @router.put("/process-routes/step/reagent/update")
  1444. async def update_step_reagents(
  1445. body: StepReagentUpdateBody,
  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. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1452. route_svc.update_step_reagents(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1453. return success_response(None, message="更新成功")
  1454. except ValueError as e:
  1455. return error_response(400, str(e))
  1456. @router.put("/process-routes/step/consumable/update")
  1457. async def update_step_consumables(
  1458. body: StepConsumableUpdateBody,
  1459. user: dict = Depends(require_admin_or_super_admin),
  1460. db: Session = Depends(get_db),
  1461. ):
  1462. try:
  1463. tenant_no = _get_tenant_no(db, user)
  1464. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1465. route_svc.update_step_consumables(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1466. return success_response(None, message="更新成功")
  1467. except ValueError as e:
  1468. return error_response(400, str(e))
  1469. @router.put("/process-routes/step/output/update")
  1470. async def update_step_outputs(
  1471. body: StepOutputUpdateBody,
  1472. user: dict = Depends(require_admin_or_super_admin),
  1473. db: Session = Depends(get_db),
  1474. ):
  1475. try:
  1476. tenant_no = _get_tenant_no(db, user)
  1477. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1478. route_svc.update_step_outputs(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1479. return success_response(None, message="更新成功")
  1480. except ValueError as e:
  1481. return error_response(400, str(e))
  1482. @router.put("/process-routes/step/data-collection/update")
  1483. async def update_step_data_collections(
  1484. body: StepDataCollectionUpdateBody,
  1485. user: dict = Depends(require_admin_or_super_admin),
  1486. db: Session = Depends(get_db),
  1487. ):
  1488. try:
  1489. tenant_no = _get_tenant_no(db, user)
  1490. items_data = [it.model_dump(by_alias=False) for it in body.items]
  1491. route_svc.update_step_data_collections(db, body.step_id, items_data, tenant_no, user.get("user_id"))
  1492. return success_response(None, message="更新成功")
  1493. except ValueError as e:
  1494. return error_response(400, str(e))
  1495. # ---------------------------------------------------------------------------
  1496. # 9. 入库生产工单
  1497. # ---------------------------------------------------------------------------
  1498. @router.post("/production-work-orders")
  1499. async def create_production_work_order(
  1500. body: ProductionWorkOrderCreate,
  1501. user: dict = Depends(require_admin_or_super_admin),
  1502. db: Session = Depends(get_db),
  1503. ):
  1504. try:
  1505. tenant_no = _get_tenant_no(db, user)
  1506. wo_type = (body.work_order_type or "inbound").strip()
  1507. if wo_type == "temporary":
  1508. wo = production_work_order_svc.create_temp_production_work_order(
  1509. db,
  1510. tenant_no=tenant_no,
  1511. user_id=user.get("user_id"),
  1512. create_dept_id=user.get("dept_id", ""),
  1513. remark=body.remark or "",
  1514. )
  1515. else:
  1516. wo = production_work_order_svc.create_production_work_order_manual(
  1517. db,
  1518. tenant_no=tenant_no,
  1519. user_id=user.get("user_id"),
  1520. create_dept_id=user.get("dept_id", ""),
  1521. protocol_no=body.protocol_no or "",
  1522. sample_code=body.sample_code or "",
  1523. reception_source=body.reception_source or "cell_storage",
  1524. remark=body.remark or "",
  1525. )
  1526. db.commit()
  1527. return success_response(production_work_order_svc._wo_to_dict(wo), message="创建成功")
  1528. except ValueError as e:
  1529. return error_response(400, str(e))
  1530. except Exception as e:
  1531. db.rollback()
  1532. return error_response(400, str(e))
  1533. @router.get("/production-work-orders")
  1534. async def list_production_work_orders(
  1535. page: int = Query(1, ge=1),
  1536. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1537. keyword: Optional[str] = None,
  1538. workOrderNo: Optional[str] = None,
  1539. workOrderStatus: Optional[str] = Query(None, description="工单状态,多个用英文逗号分隔"),
  1540. workOrderType: Optional[str] = None,
  1541. receptionSource: Optional[str] = Query(None, alias="receptionSource"),
  1542. bizType: Optional[str] = Query(None, alias="bizType"),
  1543. protocolNo: Optional[str] = None,
  1544. sampleCode: Optional[str] = None,
  1545. createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
  1546. createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
  1547. infusionTimeStart: Optional[str] = Query(None, alias="infusionTimeStart"),
  1548. infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
  1549. plannedStartTimeStart: Optional[str] = Query(None, alias="plannedStartTimeStart"),
  1550. plannedStartTimeEnd: Optional[str] = Query(None, alias="plannedStartTimeEnd"),
  1551. excludeVoidProtocol: bool = Query(True, alias="excludeVoidProtocol"),
  1552. user: dict = Depends(require_admin_or_super_admin),
  1553. db: Session = Depends(get_db),
  1554. ):
  1555. tenant_no = _get_tenant_no(db, user)
  1556. query = ProductionWorkOrderListQuery(
  1557. page=page, page_size=pageSize,
  1558. keyword=keyword,
  1559. work_order_no=workOrderNo, work_order_status=workOrderStatus,
  1560. work_order_type=workOrderType,
  1561. reception_source=receptionSource,
  1562. biz_type=bizType,
  1563. protocol_no=protocolNo, sample_code=sampleCode,
  1564. create_time_start=createTimeStart,
  1565. create_time_end=createTimeEnd,
  1566. infusion_time_start=infusionTimeStart,
  1567. infusion_time_end=infusionTimeEnd,
  1568. planned_start_time_start=plannedStartTimeStart,
  1569. planned_start_time_end=plannedStartTimeEnd,
  1570. exclude_void_protocol=excludeVoidProtocol,
  1571. )
  1572. result = production_work_order_svc.list_production_work_orders(
  1573. db, tenant_no=tenant_no, query=query, page=page, page_size=pageSize,
  1574. user_info=user,
  1575. )
  1576. items = result.get("items", [])
  1577. total = result.get("total", 0)
  1578. tp = production_task_svc.total_pages(total, pageSize)
  1579. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1580. @router.get("/production-work-orders/tab-step-counts")
  1581. async def production_work_order_tab_step_counts(
  1582. user: dict = Depends(require_admin_or_super_admin),
  1583. db: Session = Depends(get_db),
  1584. ):
  1585. try:
  1586. tenant_no = _get_tenant_no(db, user)
  1587. counts = production_task_svc.count_production_work_order_tab_step_badges(
  1588. db, tenant_no=tenant_no, user_info=user
  1589. )
  1590. return success_response(counts)
  1591. except PermissionError as e:
  1592. return error_response(403, str(e))
  1593. except Exception as e:
  1594. return error_response(400, str(e))
  1595. @router.get("/production-work-orders/mine-tab-step-counts")
  1596. async def production_work_order_mine_tab_step_counts(
  1597. user: dict = Depends(require_menu_code("production:my-task:list")),
  1598. db: Session = Depends(get_db),
  1599. ):
  1600. try:
  1601. tenant_no = _get_tenant_no(db, user)
  1602. counts = production_task_svc.count_mine_production_work_order_tab_step_badges(
  1603. db,
  1604. tenant_no=tenant_no,
  1605. assignee_user_id=_get_current_user_id(user),
  1606. )
  1607. return success_response(counts)
  1608. except PermissionError as e:
  1609. return error_response(403, str(e))
  1610. except Exception as e:
  1611. return error_response(400, str(e))
  1612. @router.get("/production-work-orders/{wo_id}")
  1613. async def get_production_work_order(
  1614. wo_id: str,
  1615. user: dict = Depends(require_admin_or_super_admin),
  1616. db: Session = Depends(get_db),
  1617. ):
  1618. tenant_no = _get_tenant_no(db, user)
  1619. wo = production_work_order_svc.get_production_work_order(db, wo_id, tenant_no)
  1620. if not wo:
  1621. return error_response(404, "工单不存在")
  1622. return success_response(production_work_order_svc._wo_to_response_dict(db, wo))
  1623. @router.put("/production-work-orders/{wo_id}")
  1624. async def update_production_work_order_api(
  1625. wo_id: str,
  1626. body: ProductionWorkOrderUpdate,
  1627. user: dict = Depends(require_admin_or_super_admin),
  1628. db: Session = Depends(get_db),
  1629. ):
  1630. try:
  1631. tenant_no = _get_tenant_no(db, user)
  1632. wo = production_work_order_svc.update_production_work_order(
  1633. db,
  1634. wo_id,
  1635. tenant_no,
  1636. user.get("user_id"),
  1637. remark=body.remark,
  1638. )
  1639. db.commit()
  1640. return success_response(
  1641. production_work_order_svc._wo_to_response_dict(db, wo),
  1642. message="保存成功",
  1643. )
  1644. except ValueError as e:
  1645. db.rollback()
  1646. return error_response(400, str(e))
  1647. except Exception as e:
  1648. db.rollback()
  1649. return error_response(400, str(e))
  1650. @router.put("/production-work-orders/{wo_id}/complete")
  1651. async def complete_production_work_order(
  1652. wo_id: str,
  1653. body: ProductionWorkOrderComplete,
  1654. user: dict = Depends(require_admin_or_super_admin),
  1655. db: Session = Depends(get_db),
  1656. ):
  1657. try:
  1658. tenant_no = _get_tenant_no(db, user)
  1659. wo = production_work_order_svc.complete_production_work_order(
  1660. db, wo_id, tenant_no, user.get("user_id"), body,
  1661. )
  1662. return success_response(production_work_order_svc._wo_to_response_dict(db, wo), message="操作成功")
  1663. except ValueError as e:
  1664. return error_response(400, str(e))
  1665. @router.delete("/production-work-orders/{wo_id}")
  1666. async def delete_production_work_order_api(
  1667. wo_id: str,
  1668. user: dict = Depends(require_admin_or_super_admin),
  1669. db: Session = Depends(get_db),
  1670. ):
  1671. try:
  1672. tenant_no = _get_tenant_no(db, user)
  1673. production_work_order_svc.delete_production_work_order(db, wo_id, tenant_no, user.get("user_id"))
  1674. return success_response(None, message="删除成功")
  1675. except ValueError as e:
  1676. return error_response(400, str(e))
  1677. @router.post("/production-work-orders/{wo_id}/configure")
  1678. async def configure_production_work_order(
  1679. wo_id: str,
  1680. body: ProductionWorkOrderConfigureBody,
  1681. user: dict = Depends(require_admin_or_super_admin),
  1682. db: Session = Depends(get_db),
  1683. ):
  1684. try:
  1685. tenant_no = _get_tenant_no(db, user)
  1686. wo = production_work_order_svc.configure_temp_production_work_order(
  1687. db,
  1688. wo_id,
  1689. task_name=body.task_name or "",
  1690. task_type=body.task_type or "cell_storage",
  1691. route_id=body.route_id,
  1692. description=body.description or "",
  1693. remark=body.remark or "",
  1694. assignee_user_id=body.assignee_user_id,
  1695. tenant_no=tenant_no,
  1696. user_id=user.get("user_id"),
  1697. create_dept_id=user.get("dept_id", ""),
  1698. )
  1699. db.commit()
  1700. return success_response(production_work_order_svc._wo_to_dict(wo), message="配置成功")
  1701. except ValueError as e:
  1702. db.rollback()
  1703. return error_response(400, str(e))
  1704. except Exception as e:
  1705. db.rollback()
  1706. return error_response(400, str(e))
  1707. @router.get("/production-work-orders/{wo_id}/cell-product-options")
  1708. async def list_production_work_order_cell_product_options(
  1709. wo_id: str,
  1710. user: dict = Depends(require_admin_or_super_admin),
  1711. db: Session = Depends(get_db),
  1712. ):
  1713. try:
  1714. tenant_no = _get_tenant_no(db, user)
  1715. items = qc_work_order_svc.list_step_output_dispatch_options_for_production_work_order(
  1716. db, production_work_order_id=wo_id, tenant_no=tenant_no
  1717. )
  1718. return success_response(items)
  1719. except ValueError as e:
  1720. return error_response(400, str(e))
  1721. @router.get("/production-work-orders/{wo_id}/qc-info")
  1722. async def get_production_work_order_qc_info_api(
  1723. wo_id: str,
  1724. user: dict = Depends(require_admin_or_super_admin),
  1725. db: Session = Depends(get_db),
  1726. ):
  1727. try:
  1728. tenant_no = _get_tenant_no(db, user)
  1729. data = qc_work_order_svc.get_production_work_order_qc_info(
  1730. db, production_work_order_id=wo_id, tenant_no=tenant_no
  1731. )
  1732. return success_response(data)
  1733. except ValueError as e:
  1734. return error_response(400, str(e))
  1735. @router.post("/production-work-orders/{wo_id}/qc-work-orders")
  1736. async def create_qc_work_order_from_production_api(
  1737. wo_id: str,
  1738. body: ProductionQcWorkOrderCreateBody,
  1739. user: dict = Depends(require_admin_or_super_admin),
  1740. db: Session = Depends(get_db),
  1741. ):
  1742. try:
  1743. tenant_no = _get_tenant_no(db, user)
  1744. wo = qc_work_order_svc.create_qc_work_order_from_production(
  1745. db,
  1746. production_work_order_id=wo_id,
  1747. production_step_output_id=body.production_step_output_id,
  1748. remark=body.remark or "",
  1749. tenant_no=tenant_no,
  1750. user_id=user.get("user_id"),
  1751. create_dept_id=user.get("dept_id", ""),
  1752. )
  1753. db.commit()
  1754. return success_response(qc_work_order_svc._wo_to_dict(wo), message="派发成功")
  1755. except ValueError as e:
  1756. db.rollback()
  1757. return error_response(400, str(e))
  1758. except Exception as e:
  1759. db.rollback()
  1760. return error_response(400, str(e))
  1761. # ---------------------------------------------------------------------------
  1762. # 10. 生产任务
  1763. # ---------------------------------------------------------------------------
  1764. @router.post("/production-tasks")
  1765. async def create_production_task(
  1766. body: ProductionTaskCreate,
  1767. user: dict = Depends(require_admin_or_super_admin),
  1768. db: Session = Depends(get_db),
  1769. ):
  1770. try:
  1771. tenant_no = _get_tenant_no(db, user)
  1772. data = body.model_dump(by_alias=False)
  1773. task = production_task_svc.create_production_task(
  1774. db, tenant_no=tenant_no, user_id=user.get("user_id"),
  1775. create_dept_id="", data=data,
  1776. )
  1777. db.commit()
  1778. result = production_task_svc._task_to_dict(task)
  1779. if data.get("route_id"):
  1780. production_task_svc._enrich_tasks_with_step_summaries(db, [result], tenant_no=tenant_no)
  1781. return success_response(result, message="创建成功")
  1782. except ValueError as e:
  1783. return error_response(400, str(e))
  1784. @router.get("/production-tasks")
  1785. async def list_production_tasks(
  1786. page: int = Query(1, ge=1),
  1787. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1788. keyword: Optional[str] = None,
  1789. workOrderId: Optional[str] = None,
  1790. taskNo: Optional[str] = None,
  1791. taskStatus: Optional[str] = None,
  1792. startedTimeStart: Optional[str] = Query(None, alias="startedTimeStart"),
  1793. startedTimeEnd: Optional[str] = Query(None, alias="startedTimeEnd"),
  1794. includeStepSummary: bool = Query(False, alias="includeStepSummary"),
  1795. user: dict = Depends(require_admin_or_super_admin),
  1796. db: Session = Depends(get_db),
  1797. ):
  1798. tenant_no = _get_tenant_no(db, user)
  1799. items, total = production_task_svc.list_production_tasks(
  1800. db, tenant_no=tenant_no, page=page, page_size=pageSize,
  1801. keyword=keyword,
  1802. work_order_id=workOrderId, task_no=taskNo, task_status=taskStatus,
  1803. started_time_start=startedTimeStart,
  1804. started_time_end=startedTimeEnd,
  1805. user_info=user,
  1806. include_step_summary=includeStepSummary,
  1807. )
  1808. tp = production_task_svc.total_pages(total, pageSize)
  1809. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1810. @router.post("/production-tasks/list-by-work-orders")
  1811. async def list_production_tasks_by_work_orders(
  1812. body: ProductionTaskListByWorkOrdersBody,
  1813. user: dict = Depends(require_admin_or_super_admin),
  1814. db: Session = Depends(get_db),
  1815. ):
  1816. tenant_no = _get_tenant_no(db, user)
  1817. wo_ids = [x.strip() for x in body.work_order_ids if x and str(x).strip()]
  1818. if not wo_ids:
  1819. return success_response(
  1820. {"list": [], "total": 0, "page": 1, "pageSize": body.page_size, "totalPages": 0}
  1821. )
  1822. effective_page_size = min(max(body.page_size, len(wo_ids) * 100), 500)
  1823. items, total = production_task_svc.list_production_tasks(
  1824. db,
  1825. tenant_no=tenant_no,
  1826. page=1,
  1827. page_size=effective_page_size,
  1828. work_order_ids=wo_ids,
  1829. user_info=user,
  1830. include_step_summary=body.include_step_summary,
  1831. )
  1832. tp = production_task_svc.total_pages(total, effective_page_size)
  1833. return success_response(
  1834. {
  1835. "list": items,
  1836. "total": total,
  1837. "page": 1,
  1838. "pageSize": effective_page_size,
  1839. "totalPages": tp,
  1840. }
  1841. )
  1842. @router.get("/production-tasks/mine")
  1843. async def list_my_production_tasks(
  1844. page: int = Query(1, ge=1),
  1845. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  1846. keyword: Optional[str] = None,
  1847. taskStatus: Optional[str] = None,
  1848. workOrderType: Optional[str] = None,
  1849. receptionSource: Optional[str] = Query(None, alias="receptionSource"),
  1850. bizType: Optional[str] = Query(None, alias="bizType"),
  1851. scope: str = Query("current", description="current|history"),
  1852. includeStepSummary: bool = Query(False, alias="includeStepSummary"),
  1853. excludeVoidProtocol: bool = Query(True, alias="excludeVoidProtocol"),
  1854. plannedStartTimeStart: Optional[str] = Query(None, alias="plannedStartTimeStart"),
  1855. plannedStartTimeEnd: Optional[str] = Query(None, alias="plannedStartTimeEnd"),
  1856. user: dict = Depends(require_menu_code("production:my-task:list")),
  1857. db: Session = Depends(get_db),
  1858. ):
  1859. tenant_no = _get_tenant_no(db, user)
  1860. items, total = production_task_svc.list_mine_production_tasks(
  1861. db,
  1862. assignee_user_id=_get_current_user_id(user),
  1863. tenant_no=tenant_no,
  1864. scope=scope,
  1865. page=page,
  1866. page_size=pageSize,
  1867. keyword=keyword,
  1868. task_status=taskStatus,
  1869. work_order_type=workOrderType,
  1870. reception_source=receptionSource,
  1871. biz_type=bizType,
  1872. include_step_summary=includeStepSummary,
  1873. exclude_void_protocol=excludeVoidProtocol,
  1874. planned_start_time_start=plannedStartTimeStart,
  1875. planned_start_time_end=plannedStartTimeEnd,
  1876. )
  1877. tp = production_task_svc.total_pages(total, pageSize)
  1878. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1879. @router.post("/production-tasks/{task_id}/forward")
  1880. async def forward_production_task_api(
  1881. task_id: str,
  1882. body: ProductionTaskForwardBody,
  1883. operateScope: Optional[str] = Query(None, alias="operateScope"),
  1884. user: dict = Depends(require_any_menu_code("production:my-task:list", "production:work-order:list")),
  1885. db: Session = Depends(get_db),
  1886. ):
  1887. try:
  1888. tenant_no = _get_tenant_no(db, user)
  1889. scope = (operateScope or "mine").strip().lower()
  1890. require_assignee = scope == "mine"
  1891. from_uid = _get_current_user_id(user) if require_assignee else (user.get("user_id") or "")
  1892. task = production_task_svc.forward_production_task(
  1893. db,
  1894. task_id,
  1895. from_user_id=from_uid,
  1896. to_user_id=body.to_user_id,
  1897. tenant_no=tenant_no,
  1898. operator_id=user.get("user_id"),
  1899. create_dept_id=user.get("dept_id", ""),
  1900. remark=body.remark,
  1901. require_assignee=require_assignee,
  1902. )
  1903. db.commit()
  1904. item = production_task_svc._task_to_dict(task)
  1905. production_task_svc._enrich_assignee_names(db, [item])
  1906. return success_response(item, message="转发成功")
  1907. except PermissionError as e:
  1908. db.rollback()
  1909. return error_response(403, str(e))
  1910. except ValueError as e:
  1911. db.rollback()
  1912. return error_response(400, str(e))
  1913. @router.post("/production-tasks/{task_id}/spawn-temporary-work-order")
  1914. async def spawn_temporary_work_order_from_task_api(
  1915. task_id: str,
  1916. body: SpawnTemporaryWorkOrderBody,
  1917. operateScope: Optional[str] = Query(None, alias="operateScope"),
  1918. user: dict = Depends(
  1919. require_any_menu_code("production:my-task:list", "production:work-order:list")
  1920. ),
  1921. db: Session = Depends(get_db),
  1922. ):
  1923. try:
  1924. tenant_no = _get_tenant_no(db, user)
  1925. result = production_task_svc.spawn_temporary_work_order_from_task(
  1926. db,
  1927. task_id,
  1928. tenant_no=tenant_no,
  1929. user_id=user.get("user_id"),
  1930. create_dept_id=user.get("dept_id", ""),
  1931. remark=body.remark or "",
  1932. )
  1933. db.commit()
  1934. return success_response(result, message="临时工单已创建")
  1935. except ValueError as e:
  1936. db.rollback()
  1937. return error_response(400, str(e))
  1938. @router.put("/production-tasks/{task_id}/assignee")
  1939. async def reassign_production_task_api(
  1940. task_id: str,
  1941. body: ProductionTaskAssigneeUpdate,
  1942. user: dict = Depends(require_menu_code("production:work-order:list")),
  1943. db: Session = Depends(get_db),
  1944. ):
  1945. try:
  1946. tenant_no = _get_tenant_no(db, user)
  1947. task = production_task_svc.reassign_production_task(
  1948. db,
  1949. task_id,
  1950. assignee_user_id=body.assignee_user_id,
  1951. tenant_no=tenant_no,
  1952. operator_id=user.get("user_id"),
  1953. create_dept_id=user.get("dept_id", ""),
  1954. remark=body.remark,
  1955. )
  1956. db.commit()
  1957. item = production_task_svc._task_to_dict(task)
  1958. production_task_svc._enrich_assignee_names(db, [item])
  1959. return success_response(item, message="改派成功")
  1960. except ValueError as e:
  1961. db.rollback()
  1962. return error_response(400, str(e))
  1963. @router.put("/production-tasks/{task_id}/steps/dispatch")
  1964. async def dispatch_production_task_steps_api(
  1965. task_id: str,
  1966. body: ProductionTaskStepDispatchBody,
  1967. operateScope: Optional[str] = Query(None, alias="operateScope"),
  1968. user: dict = Depends(require_any_menu_code("production:my-task:list", "production:work-order:list")),
  1969. db: Session = Depends(get_db),
  1970. ):
  1971. try:
  1972. tenant_no = _get_tenant_no(db, user)
  1973. payload = [
  1974. {
  1975. "step_id": it.step_id,
  1976. "assignee_user_ids": it.assignee_user_ids,
  1977. "planned_start_time": it.planned_start_time,
  1978. }
  1979. for it in body.steps
  1980. ]
  1981. detail = production_task_svc.dispatch_task_steps(
  1982. db,
  1983. task_id,
  1984. tenant_no=tenant_no,
  1985. steps_payload=payload,
  1986. operator_id=user.get("user_id"),
  1987. create_dept_id=user.get("dept_id", ""),
  1988. )
  1989. db.commit()
  1990. return success_response(detail, message="步骤派发成功")
  1991. except ValueError as e:
  1992. db.rollback()
  1993. return error_response(400, str(e))
  1994. @router.get("/production-tasks/{task_id}/editor-init")
  1995. async def get_production_task_editor_init(
  1996. task_id: str,
  1997. includeReference: bool = Query(True, alias="includeReference"),
  1998. user: dict = Depends(require_admin_or_super_admin),
  1999. db: Session = Depends(get_db),
  2000. ):
  2001. """编辑生产任务弹窗初始化:任务详情 + 工艺库 + 步骤配置参考数据(聚合接口)。"""
  2002. tenant_no = _get_tenant_no(db, user)
  2003. payload = production_task_svc.get_production_task_editor_init(
  2004. db,
  2005. task_id,
  2006. tenant_no,
  2007. user_info=user,
  2008. include_reference=includeReference,
  2009. )
  2010. if not payload:
  2011. return error_response(404, "任务不存在")
  2012. return success_response(payload)
  2013. @router.get("/production-tasks/{task_id}")
  2014. async def get_production_task_detail(
  2015. task_id: str,
  2016. includeStepSubTables: bool = Query(True, alias="includeStepSubTables"),
  2017. user: dict = Depends(require_admin_or_super_admin),
  2018. db: Session = Depends(get_db),
  2019. ):
  2020. tenant_no = _get_tenant_no(db, user)
  2021. detail = production_task_svc.get_production_task_detail(
  2022. db,
  2023. task_id,
  2024. tenant_no,
  2025. include_step_sub_tables=includeStepSubTables,
  2026. )
  2027. if not detail:
  2028. return error_response(404, "任务不存在")
  2029. return success_response(detail)
  2030. @router.put("/production-tasks/{task_id}")
  2031. async def update_production_task(
  2032. task_id: str,
  2033. body: ProductionTaskUpdate,
  2034. user: dict = Depends(require_admin_or_super_admin),
  2035. db: Session = Depends(get_db),
  2036. ):
  2037. try:
  2038. tenant_no = _get_tenant_no(db, user)
  2039. data = body.model_dump(exclude_unset=True, by_alias=False)
  2040. task = production_task_svc.update_production_task(
  2041. db, task_id, tenant_no, user.get("user_id"), data,
  2042. )
  2043. return success_response(production_task_svc._task_to_dict(task), message="更新成功")
  2044. except ValueError as e:
  2045. return error_response(400, str(e))
  2046. @router.delete("/production-tasks/{task_id}")
  2047. async def delete_production_task(
  2048. task_id: str,
  2049. user: dict = Depends(require_admin_or_super_admin),
  2050. db: Session = Depends(get_db),
  2051. ):
  2052. try:
  2053. tenant_no = _get_tenant_no(db, user)
  2054. production_task_svc.delete_production_task(db, task_id, tenant_no, user.get("user_id"))
  2055. return success_response(None, message="删除成功")
  2056. except ValueError as e:
  2057. return error_response(400, str(e))
  2058. @router.put("/production-tasks/{task_id}/status")
  2059. async def update_production_task_status(
  2060. task_id: str,
  2061. body: ProductionTaskStatusUpdate,
  2062. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2063. user: dict = Depends(require_admin_or_super_admin),
  2064. db: Session = Depends(get_db),
  2065. ):
  2066. try:
  2067. tenant_no = _get_tenant_no(db, user)
  2068. task = production_task_svc.update_task_status(
  2069. db,
  2070. task_id,
  2071. tenant_no,
  2072. user.get("user_id"),
  2073. body.task_status,
  2074. conclusion=body.conclusion,
  2075. remark=body.remark,
  2076. )
  2077. return success_response(production_task_svc._task_to_dict(task), message="操作成功")
  2078. except PermissionError as e:
  2079. return error_response(403, str(e))
  2080. except ValueError as e:
  2081. return error_response(400, str(e))
  2082. @router.post("/production-tasks/{task_id}/steps")
  2083. async def add_production_task_step(
  2084. task_id: str,
  2085. body: TaskStepAdd,
  2086. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2087. user: dict = Depends(require_admin_or_super_admin),
  2088. db: Session = Depends(get_db),
  2089. ):
  2090. try:
  2091. tenant_no = _get_tenant_no(db, user)
  2092. data = body.model_dump(by_alias=False)
  2093. step_data = production_task_svc.add_task_step(
  2094. db, task_id, tenant_no, user.get("user_id"), data,
  2095. )
  2096. return success_response(step_data, message="添加成功")
  2097. except PermissionError as e:
  2098. return error_response(403, str(e))
  2099. except ValueError as e:
  2100. return error_response(400, str(e))
  2101. @router.put("/production-tasks/{task_id}/steps/{step_id}")
  2102. async def update_production_task_step(
  2103. task_id: str,
  2104. step_id: str,
  2105. body: TaskStepUpdate,
  2106. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2107. user: dict = Depends(require_admin_or_super_admin),
  2108. db: Session = Depends(get_db),
  2109. ):
  2110. try:
  2111. tenant_no = _get_tenant_no(db, user)
  2112. data = body.model_dump(exclude_unset=True, by_alias=False)
  2113. step = production_task_svc.update_task_step(
  2114. db, step_id, tenant_no, user.get("user_id"), data,
  2115. )
  2116. return success_response(production_task_svc._step_to_dict(step), message="更新成功")
  2117. except PermissionError as e:
  2118. return error_response(403, str(e))
  2119. except ValueError as e:
  2120. return error_response(400, str(e))
  2121. @router.delete("/production-tasks/{task_id}/steps/{step_id}")
  2122. async def delete_production_task_step(
  2123. task_id: str,
  2124. step_id: str,
  2125. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2126. user: dict = Depends(require_admin_or_super_admin),
  2127. db: Session = Depends(get_db),
  2128. ):
  2129. try:
  2130. tenant_no = _get_tenant_no(db, user)
  2131. production_task_svc.delete_task_step(db, step_id, tenant_no, user.get("user_id"))
  2132. return success_response(None, message="删除成功")
  2133. except PermissionError as e:
  2134. return error_response(403, str(e))
  2135. except ValueError as e:
  2136. return error_response(400, str(e))
  2137. @router.put("/production-tasks/step/reorder")
  2138. async def reorder_production_task_steps(
  2139. body: TaskStepReorderBody,
  2140. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2141. user: dict = Depends(require_admin_or_super_admin),
  2142. db: Session = Depends(get_db),
  2143. ):
  2144. try:
  2145. tenant_no = _get_tenant_no(db, user)
  2146. production_task_svc.reorder_task_steps(db, body.task_id, body.step_ids)
  2147. return success_response(None, message="排序成功")
  2148. except PermissionError as e:
  2149. return error_response(403, str(e))
  2150. except ValueError as e:
  2151. return error_response(400, str(e))
  2152. @router.post("/production-tasks/{task_id}/steps/{step_id}/copy")
  2153. async def copy_production_task_step(
  2154. task_id: str,
  2155. step_id: str,
  2156. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2157. user: dict = Depends(require_admin_or_super_admin),
  2158. db: Session = Depends(get_db),
  2159. ):
  2160. try:
  2161. tenant_no = _get_tenant_no(db, user)
  2162. step_data = production_task_svc.copy_task_step(
  2163. db,
  2164. step_id=step_id,
  2165. tenant_no=tenant_no,
  2166. user_id=user.get("user_id"),
  2167. )
  2168. return success_response(step_data, message="复制成功")
  2169. except PermissionError as e:
  2170. return error_response(403, str(e))
  2171. except ValueError as e:
  2172. return error_response(400, str(e))
  2173. @router.put("/production-tasks/{task_id}/steps/{step_id}/reagents")
  2174. async def replace_task_step_reagents(
  2175. task_id: str,
  2176. step_id: str,
  2177. body: List[TaskStepMaterialItem],
  2178. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2179. user: dict = Depends(require_admin_or_super_admin),
  2180. db: Session = Depends(get_db),
  2181. ):
  2182. try:
  2183. tenant_no = _get_tenant_no(db, user)
  2184. items_data = []
  2185. for it in body:
  2186. d = it.model_dump(by_alias=False)
  2187. items_data.append({
  2188. "reagent_code": d.get("material_code"),
  2189. "reagent_name": d.get("material_name"),
  2190. "spec": d.get("spec"),
  2191. "standard_usage": d.get("standard_usage"),
  2192. "actual_usage": d.get("actual_usage"),
  2193. "unit": d.get("unit"),
  2194. "requisition_usage_id": d.get("requisition_usage_id"),
  2195. "is_key_material": d.get("is_key_material", "0"),
  2196. "remark": d.get("remark"),
  2197. })
  2198. production_task_svc.replace_step_reagents(db, step_id, tenant_no, user.get("user_id"), items_data)
  2199. return success_response(None, message="更新成功")
  2200. except PermissionError as e:
  2201. return error_response(403, str(e))
  2202. except ValueError as e:
  2203. return error_response(400, str(e))
  2204. @router.put("/production-tasks/{task_id}/steps/{step_id}/consumables")
  2205. async def replace_task_step_consumables(
  2206. task_id: str,
  2207. step_id: str,
  2208. body: List[TaskStepMaterialItem],
  2209. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2210. user: dict = Depends(require_admin_or_super_admin),
  2211. db: Session = Depends(get_db),
  2212. ):
  2213. try:
  2214. tenant_no = _get_tenant_no(db, user)
  2215. items_data = []
  2216. for it in body:
  2217. d = it.model_dump(by_alias=False)
  2218. items_data.append({
  2219. "consumable_code": d.get("material_code"),
  2220. "consumable_name": d.get("material_name"),
  2221. "spec_model": d.get("spec_model"),
  2222. "standard_usage": d.get("standard_usage"),
  2223. "actual_usage": d.get("actual_usage"),
  2224. "unit": d.get("unit"),
  2225. "requisition_usage_id": d.get("requisition_usage_id"),
  2226. "is_required": d.get("is_required", "1"),
  2227. "remark": d.get("remark"),
  2228. })
  2229. production_task_svc.replace_step_consumables(db, step_id, tenant_no, user.get("user_id"), items_data)
  2230. return success_response(None, message="更新成功")
  2231. except PermissionError as e:
  2232. return error_response(403, str(e))
  2233. except ValueError as e:
  2234. return error_response(400, str(e))
  2235. @router.put("/production-tasks/{task_id}/steps/{step_id}/configured-reagents")
  2236. async def replace_task_step_configured_reagents(
  2237. task_id: str,
  2238. step_id: str,
  2239. body: List[TaskStepMaterialItem],
  2240. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2241. user: dict = Depends(require_admin_or_super_admin),
  2242. db: Session = Depends(get_db),
  2243. ):
  2244. try:
  2245. tenant_no = _get_tenant_no(db, user)
  2246. items_data = []
  2247. for it in body:
  2248. d = it.model_dump(by_alias=False)
  2249. items_data.append({
  2250. "configured_reagent_code": d.get("material_code"),
  2251. "configured_reagent_name": d.get("material_name"),
  2252. "spec": d.get("spec"),
  2253. "standard_usage": d.get("standard_usage"),
  2254. "actual_usage": d.get("actual_usage"),
  2255. "unit": d.get("unit"),
  2256. "requisition_usage_id": d.get("requisition_usage_id"),
  2257. "remark": d.get("remark"),
  2258. })
  2259. production_task_svc.replace_step_configured_reagents(
  2260. db, step_id, tenant_no, user.get("user_id"), items_data
  2261. )
  2262. return success_response(None, message="更新成功")
  2263. except PermissionError as e:
  2264. return error_response(403, str(e))
  2265. except ValueError as e:
  2266. return error_response(400, str(e))
  2267. @router.put("/production-tasks/{task_id}/steps/{step_id}/data-collection")
  2268. async def replace_task_step_data_collections(
  2269. task_id: str,
  2270. step_id: str,
  2271. body: List[TaskStepDataCollectionItem],
  2272. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2273. user: dict = Depends(require_admin_or_super_admin),
  2274. db: Session = Depends(get_db),
  2275. ):
  2276. try:
  2277. tenant_no = _get_tenant_no(db, user)
  2278. items_data = [it.model_dump(by_alias=False) for it in body]
  2279. production_task_svc.replace_step_data_collections(db, step_id, tenant_no, user.get("user_id"), items_data)
  2280. return success_response(None, message="更新成功")
  2281. except PermissionError as e:
  2282. return error_response(403, str(e))
  2283. except ValueError as e:
  2284. return error_response(400, str(e))
  2285. @router.put("/production-tasks/{task_id}/steps/{step_id}/outputs")
  2286. async def replace_task_step_outputs(
  2287. task_id: str,
  2288. step_id: str,
  2289. body: List[TaskStepOutputItem],
  2290. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2291. user: dict = Depends(require_admin_or_super_admin),
  2292. db: Session = Depends(get_db),
  2293. ):
  2294. try:
  2295. tenant_no = _get_tenant_no(db, user)
  2296. items_data = [it.model_dump(by_alias=False) for it in body]
  2297. production_task_svc.replace_step_outputs(db, step_id, tenant_no, user.get("user_id"), items_data)
  2298. return success_response(None, message="更新成功")
  2299. except PermissionError as e:
  2300. return error_response(403, str(e))
  2301. except ValueError as e:
  2302. return error_response(400, str(e))
  2303. @router.post("/production-tasks/{task_id}/inputs")
  2304. async def set_production_task_inputs(
  2305. task_id: str,
  2306. body: List[TaskInputItem],
  2307. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2308. user: dict = Depends(require_admin_or_super_admin),
  2309. db: Session = Depends(get_db),
  2310. ):
  2311. try:
  2312. tenant_no = _get_tenant_no(db, user)
  2313. items_data = [it.model_dump(by_alias=False) for it in body]
  2314. production_task_svc.set_task_inputs(db, task_id, tenant_no, user.get("user_id"), items_data)
  2315. return success_response(None, message="设置成功")
  2316. except PermissionError as e:
  2317. return error_response(403, str(e))
  2318. except ValueError as e:
  2319. return error_response(400, str(e))
  2320. @router.post("/production-tasks/{task_id}/outputs")
  2321. async def set_production_task_outputs(
  2322. task_id: str,
  2323. body: List[TaskOutputItem],
  2324. operateScope: Optional[str] = Query(None, alias="operateScope"),
  2325. user: dict = Depends(require_admin_or_super_admin),
  2326. db: Session = Depends(get_db),
  2327. ):
  2328. try:
  2329. tenant_no = _get_tenant_no(db, user)
  2330. items_data = [it.model_dump(by_alias=False) for it in body]
  2331. production_task_svc.set_task_outputs(db, task_id, tenant_no, user.get("user_id"), items_data)
  2332. return success_response(None, message="设置成功")
  2333. except PermissionError as e:
  2334. return error_response(403, str(e))
  2335. except ValueError as e:
  2336. return error_response(400, str(e))
  2337. # ---------------------------------------------------------------------------
  2338. # 11. 细胞产品库
  2339. # ---------------------------------------------------------------------------
  2340. @router.get("/cell-products/next-code")
  2341. async def get_next_cell_product_code(
  2342. count: int = Query(1, ge=1, le=50),
  2343. user: dict = Depends(require_admin_or_super_admin),
  2344. db: Session = Depends(get_db),
  2345. ):
  2346. tenant_no = _get_tenant_no(db, user)
  2347. if count <= 1:
  2348. code = cell_product_svc.get_next_code(db, tenant_no)
  2349. return success_response({"code": code})
  2350. codes = cell_product_svc.reserve_next_codes(db, tenant_no, count)
  2351. return success_response({"codes": codes, "code": codes[0] if codes else ""})
  2352. @router.get("/cell-products/max-passage-seq")
  2353. async def get_cell_product_max_passage_seq(
  2354. productCode: str = Query(..., alias="productCode"),
  2355. passage: Optional[str] = None,
  2356. passages: Optional[str] = None,
  2357. user: dict = Depends(require_admin_or_super_admin),
  2358. db: Session = Depends(get_db),
  2359. ):
  2360. """查询 `{productCode}-{代次}-XX` 在细胞产品库中的最大两位序号。"""
  2361. tenant_no = _get_tenant_no(db, user)
  2362. base = (productCode or "").strip()
  2363. if not base:
  2364. return error_response(400, "产品编码不能为空")
  2365. passage_list: List[str] = []
  2366. if passages:
  2367. passage_list.extend(p.strip() for p in passages.split(",") if p and p.strip())
  2368. if passage and passage.strip():
  2369. passage_list.append(passage.strip())
  2370. if not passage_list:
  2371. return error_response(400, "请指定代次")
  2372. max_seq_map = cell_product_svc.get_max_passage_product_seq_map(
  2373. db, tenant_no, base, passage_list
  2374. )
  2375. next_seq_map = {p: max_seq_map.get(p, 0) + 1 for p in max_seq_map}
  2376. if len(max_seq_map) == 1:
  2377. only_passage = next(iter(max_seq_map))
  2378. only_max = max_seq_map[only_passage]
  2379. only_next = next_seq_map[only_passage]
  2380. return success_response(
  2381. {
  2382. "passage": only_passage,
  2383. "maxSeq": only_max,
  2384. "nextSeq": only_next,
  2385. "maxSeqByPassage": max_seq_map,
  2386. "nextSeqByPassage": next_seq_map,
  2387. }
  2388. )
  2389. return success_response(
  2390. {"maxSeqByPassage": max_seq_map, "nextSeqByPassage": next_seq_map}
  2391. )
  2392. @router.get("/cell-products/max-suffix-seq")
  2393. async def get_cell_product_max_suffix_seq(
  2394. productCode: str = Query(..., alias="productCode"),
  2395. user: dict = Depends(require_admin_or_super_admin),
  2396. db: Session = Depends(get_db),
  2397. ):
  2398. """查询 `{productCode}-XX` 在细胞产品库中的最大两位序号。"""
  2399. tenant_no = _get_tenant_no(db, user)
  2400. base = (productCode or "").strip()
  2401. if not base:
  2402. return error_response(400, "产品编码不能为空")
  2403. max_seq = cell_product_svc.get_max_product_suffix_seq(db, tenant_no, base)
  2404. return success_response({"maxSeq": max_seq, "nextSeq": max_seq + 1})
  2405. @router.get("/cell-products/check-code")
  2406. async def check_cell_product_code(
  2407. productCode: str = Query(..., alias="productCode"),
  2408. excludeId: Optional[str] = Query(None, alias="excludeId"),
  2409. user: dict = Depends(require_admin_or_super_admin),
  2410. db: Session = Depends(get_db),
  2411. ):
  2412. """检查产品编码是否已被占用(编辑时可排除当前记录)。"""
  2413. tenant_no = _get_tenant_no(db, user)
  2414. code = (productCode or "").strip()
  2415. if not code:
  2416. return error_response(400, "产品编码不能为空")
  2417. exists = cell_product_svc.product_code_exists(
  2418. db, tenant_no, code, exclude_id=excludeId
  2419. )
  2420. return success_response({"exists": exists, "productCode": code})
  2421. @router.get("/cell-products")
  2422. async def list_cell_products(
  2423. page: int = Query(1, ge=1),
  2424. pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
  2425. keyword: Optional[str] = None,
  2426. productCode: Optional[str] = None,
  2427. productName: Optional[str] = None,
  2428. productTypeId: Optional[str] = None,
  2429. productStatus: Optional[str] = None,
  2430. passage: Optional[str] = None,
  2431. sourceType: Optional[str] = None,
  2432. user: dict = Depends(require_admin_or_super_admin),
  2433. db: Session = Depends(get_db),
  2434. ):
  2435. tenant_no = _get_tenant_no(db, user)
  2436. result = cell_product_svc.list_cell_products(
  2437. db, tenant_no=tenant_no, page=page, page_size=pageSize,
  2438. keyword=keyword,
  2439. product_code=productCode, product_name=productName,
  2440. product_type_id=productTypeId, product_status=productStatus,
  2441. passage=passage, source_type=sourceType,
  2442. user_info=user,
  2443. )
  2444. items = result.get("items", [])
  2445. total = result.get("total", 0)
  2446. tp = cell_product_svc.total_pages(total, pageSize)
  2447. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  2448. @router.get("/cell-products/{product_id}")
  2449. async def get_cell_product_detail(
  2450. product_id: str,
  2451. user: dict = Depends(require_admin_or_super_admin),
  2452. db: Session = Depends(get_db),
  2453. ):
  2454. tenant_no = _get_tenant_no(db, user)
  2455. obj = cell_product_svc.get_cell_product(db, product_id, tenant_no)
  2456. if not obj:
  2457. return error_response(404, "细胞产品不存在")
  2458. return success_response(cell_product_svc.product_to_response(db, tenant_no, obj))
  2459. @router.get("/cell-products/{product_id}/chain")
  2460. async def get_cell_product_chain(
  2461. product_id: str,
  2462. user: dict = Depends(require_admin_or_super_admin),
  2463. db: Session = Depends(get_db),
  2464. ):
  2465. tenant_no = _get_tenant_no(db, user)
  2466. chain = cell_product_svc.get_product_chain(db, product_id, tenant_no)
  2467. if not chain:
  2468. raise HTTPException(status_code=404, detail="细胞产品不存在")
  2469. return success_response(chain)
  2470. @router.get("/cell-products/{product_id}/lineage-qc")
  2471. async def get_cell_product_lineage_qc(
  2472. product_id: str,
  2473. user: dict = Depends(require_admin_or_super_admin),
  2474. db: Session = Depends(get_db),
  2475. ):
  2476. tenant_no = _get_tenant_no(db, user)
  2477. data = cell_product_svc.get_product_lineage_qc(db, product_id, tenant_no)
  2478. if not data:
  2479. raise HTTPException(status_code=404, detail="细胞产品不存在")
  2480. return success_response(data)
  2481. @router.post("/cell-products")
  2482. async def create_cell_product(
  2483. body: CellProductCreate,
  2484. user: dict = Depends(require_admin_or_super_admin),
  2485. db: Session = Depends(get_db),
  2486. ):
  2487. try:
  2488. tenant_no = _get_tenant_no(db, user)
  2489. items = body.items or []
  2490. qty = int(body.quantity) if body.quantity is not None else 1
  2491. qty = max(1, min(qty, 50))
  2492. if items:
  2493. created = cell_product_svc.create_cell_products_batch(
  2494. db,
  2495. tenant_no=tenant_no,
  2496. user_id=user.get("user_id"),
  2497. create_dept_id="",
  2498. data=body,
  2499. items=items,
  2500. require_storage_slot=False,
  2501. )
  2502. elif qty > 1:
  2503. created = cell_product_svc.create_cell_products_by_quantity(
  2504. db,
  2505. tenant_no=tenant_no,
  2506. user_id=user.get("user_id"),
  2507. create_dept_id="",
  2508. data=body,
  2509. count=qty,
  2510. )
  2511. else:
  2512. obj = cell_product_svc.create_cell_product(
  2513. db, tenant_no=tenant_no, user_id=user.get("user_id"),
  2514. create_dept_id="", data=body,
  2515. )
  2516. return success_response(
  2517. cell_product_svc.product_to_response(db, tenant_no, obj), message="创建成功"
  2518. )
  2519. resp_list = [
  2520. cell_product_svc.product_to_response(db, tenant_no, obj) for obj in created
  2521. ]
  2522. n = len(created)
  2523. return success_response(
  2524. {"list": resp_list, "count": n},
  2525. message=f"成功创建 {n} 份细胞产品",
  2526. )
  2527. except ValueError as e:
  2528. return error_response(400, str(e))
  2529. @router.put("/cell-products/{product_id}")
  2530. async def update_cell_product(
  2531. product_id: str,
  2532. body: CellProductUpdate,
  2533. user: dict = Depends(require_admin_or_super_admin),
  2534. db: Session = Depends(get_db),
  2535. ):
  2536. try:
  2537. tenant_no = _get_tenant_no(db, user)
  2538. obj = cell_product_svc.update_cell_product(
  2539. db, product_id, tenant_no, user.get("user_id"), data=body,
  2540. )
  2541. return success_response(
  2542. cell_product_svc.product_to_response(db, tenant_no, obj), message="更新成功"
  2543. )
  2544. except ValueError as e:
  2545. return error_response(400, str(e))
  2546. @router.delete("/cell-products/{product_id}")
  2547. async def delete_cell_product(
  2548. product_id: str,
  2549. user: dict = Depends(require_admin_or_super_admin),
  2550. db: Session = Depends(get_db),
  2551. ):
  2552. try:
  2553. tenant_no = _get_tenant_no(db, user)
  2554. cell_product_svc.delete_cell_product(db, product_id, tenant_no, user.get("user_id"))
  2555. return success_response(None, message="删除成功")
  2556. except ValueError as e:
  2557. return error_response(400, str(e))
  2558. from app.api.production_material import router as production_material_router
  2559. router.include_router(production_material_router)