production.py 114 KB

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