| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147 |
- """生产管理 API 路由(7 个子模块:耗材、试剂、SOP文档、工艺工序分类、产出物类型、耗材包、工艺库)"""
- from fastapi import APIRouter, Depends, Query, HTTPException
- from pydantic import BaseModel, ConfigDict, Field
- from sqlalchemy.orm import Session
- from typing import Any, Optional, List
- from app.core.database import get_db
- from app.core.menu_guard import (
- require_admin_or_super_admin,
- require_any_menu_code,
- require_menu_code,
- )
- from app.core.response import success_response, error_response
- from app.crud import consumable as consumable_svc
- from app.crud import reagent as reagent_svc
- from app.crud import consumable_stock_log as consumable_stock_log_svc
- from app.crud import reagent_stock_log as reagent_stock_log_svc
- from app.crud import sop_document as sop_document_svc
- from app.crud import process_category as process_category_svc
- from app.crud import equipment as equipment_svc
- from app.crud import output_type as output_type_svc
- from app.crud import production_product_type as production_product_type_svc
- from app.crud import consumable_pack as consumable_pack_svc
- from app.crud import process as process_svc
- from app.crud import process_route as route_svc
- from app.schemas.process_route import (
- ProcessRouteCreateBody,
- ProcessRouteUpdateBody,
- SetCurrentBody,
- StepAddBody,
- StepCopyBody,
- StepUpdateBody,
- StepReorderBody,
- StepReagentUpdateBody,
- StepConsumableUpdateBody,
- StepOutputUpdateBody,
- StepDataCollectionUpdateBody,
- )
- from app.models.models import SysUser
- from app.crud import production_work_order as production_work_order_svc
- from app.crud import production_task as production_task_svc
- from app.crud import production_task_set as production_task_set_svc
- from app.crud import production_waste_output as production_waste_output_svc
- from app.crud import cell_product as cell_product_svc
- from app.crud import qc_work_order as qc_work_order_svc
- from app.schemas.production_work_order import (
- ProductionWorkOrderListQuery,
- ProductionWorkOrderComplete,
- ProductionWorkOrderCreate,
- ProductionWorkOrderConfigureBody,
- ProductionWorkOrderUpdate,
- )
- from app.schemas.production_task_set import (
- ProductionTaskSetCreateBody,
- ProductionTaskSetFromWorkOrderBody,
- ProductionTaskSetUpdateBody,
- ProductionTasksFromSetBody,
- )
- from app.schemas.production_task import (
- ProductionTaskCreate,
- ProductionTaskUpdate,
- ProductionTaskStatusUpdate,
- ProductionTaskForwardBody,
- ProductionTaskAssigneeUpdate,
- ProductionTaskStepDispatchBody,
- SpawnTemporaryWorkOrderBody,
- TaskStepAdd,
- TaskStepUpdate,
- TaskStepMaterialItem,
- TaskStepOutputItem,
- TaskStepDataCollectionItem,
- TaskInputItem,
- TaskOutputItem,
- TaskStepReorderBody,
- ProductionTaskListByWorkOrdersBody,
- )
- from app.schemas.cell_product import CellProductCreate, CellProductUpdate
- from app.schemas.qc_work_order import ProductionQcWorkOrderCreateBody
- router = APIRouter(prefix="/web/admin", tags=["生产管理"])
- _LEGACY_MATERIAL_GONE_MSG = "耗材/试剂维护已迁移至【生产物料库】,请使用 production-material 接口"
- def _legacy_material_write_gone():
- return error_response(410, _LEGACY_MATERIAL_GONE_MSG)
- def _get_tenant_no(db: Session, user: dict) -> str:
- user_id = user.get("user_id")
- if not user_id:
- return "zkxw"
- sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
- return (sys_user.tenant_no if sys_user else None) or "zkxw"
- def _get_current_user_id(user: dict) -> str:
- return (user.get("user_id") or "").strip()
- # ---------------------------------------------------------------------------
- # 1. 耗材
- # ---------------------------------------------------------------------------
- class ConsumableCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- consumable_code: str = Field(..., alias="consumableCode")
- consumable_name: str = Field(..., alias="consumableName")
- spec_model: Optional[str] = Field(None, alias="specModel")
- standard_usage: Optional[str] = Field(None, alias="standardUsage")
- unit: Optional[str] = None
- description: Optional[str] = None
- warning_qty: Optional[float] = Field(0, alias="warningQty")
- status: Optional[str] = Field("1", alias="status")
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class ConsumableUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- consumable_code: Optional[str] = Field(None, alias="consumableCode")
- consumable_name: Optional[str] = Field(None, alias="consumableName")
- spec_model: Optional[str] = Field(None, alias="specModel")
- standard_usage: Optional[str] = Field(None, alias="standardUsage")
- unit: Optional[str] = None
- description: Optional[str] = None
- warning_qty: Optional[float] = Field(None, alias="warningQty")
- status: Optional[str] = Field(None, alias="status")
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- @router.get("/consumables")
- async def list_consumables(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:consumable:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = consumable_svc.list_consumables(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
- tp = consumable_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/consumables/all")
- async def list_all_consumables(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, _ = consumable_svc.list_consumables(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
- return success_response(items)
- @router.post("/consumables")
- async def create_consumable(
- body: ConsumableCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- return _legacy_material_write_gone()
- @router.put("/consumables/{consumable_id}")
- async def update_consumable(
- consumable_id: str,
- body: ConsumableUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- return _legacy_material_write_gone()
- @router.delete("/consumables/{consumable_id}")
- async def delete_consumable(
- consumable_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- return _legacy_material_write_gone()
- class StockInBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- qty: float = Field(..., gt=0)
- remark: Optional[str] = None
- class StockOutBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- qty: float = Field(..., gt=0)
- receiver: str = Field(..., min_length=1)
- purpose: Optional[str] = None
- remark: Optional[str] = None
- class BatchStockInItem(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- materialId: str
- qty: float = Field(..., gt=0)
- remark: Optional[str] = None
- class BatchStockInBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- items: List[BatchStockInItem]
- remark: Optional[str] = None
- class BatchStockOutItem(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- materialId: str
- qty: float = Field(..., gt=0)
- class BatchStockOutBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- items: List[BatchStockOutItem]
- receiver: str = Field(..., min_length=1)
- purpose: Optional[str] = None
- remark: Optional[str] = None
- @router.post("/consumables/{consumable_id}/stock-in")
- async def consumable_stock_in(
- consumable_id: str,
- body: StockInBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- from decimal import Decimal
- obj = consumable_svc.stock_in(
- db, consumable_id,
- qty=Decimal(str(body.qty)),
- remark=body.remark,
- user_id=user.get("user_id"),
- tenant_no=tenant_no,
- )
- return success_response(consumable_svc._row_to_dict(obj), message="入库成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/consumables/{consumable_id}/stock-out")
- async def consumable_stock_out(
- consumable_id: str,
- body: StockOutBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- from decimal import Decimal
- obj = consumable_svc.stock_out(
- db, consumable_id,
- qty=Decimal(str(body.qty)),
- receiver=body.receiver,
- purpose=body.purpose,
- remark=body.remark,
- user_id=user.get("user_id"),
- tenant_no=tenant_no,
- )
- return success_response(consumable_svc._row_to_dict(obj), message="出库成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/consumables/batch-stock-in")
- async def consumable_batch_stock_in(
- body: BatchStockInBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump() for it in body.items]
- count = consumable_svc.batch_stock_in(
- db, items=items_data, remark=body.remark,
- tenant_no=tenant_no, user_id=user.get("user_id"),
- )
- return success_response({"count": count}, message=f"成功入库 {count} 条")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/consumables/batch-stock-out")
- async def consumable_batch_stock_out(
- body: BatchStockOutBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump() for it in body.items]
- count = consumable_svc.batch_stock_out(
- db, items=items_data,
- receiver=body.receiver, purpose=body.purpose, remark=body.remark,
- tenant_no=tenant_no, user_id=user.get("user_id"),
- )
- return success_response({"count": count}, message=f"成功出库 {count} 条")
- except ValueError as e:
- return error_response(400, str(e))
- @router.get("/consumables/{consumable_id}/stock-logs")
- async def list_consumable_stock_logs(
- consumable_id: str,
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- changeType: Optional[str] = None,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = consumable_stock_log_svc.list_stock_logs(
- db, tenant_no, page=page, page_size=pageSize,
- material_id=consumable_id, change_type=changeType,
- user_info=user,
- )
- tp = (total + pageSize - 1) // pageSize if total > 0 else 0
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/consumables/stock-logs")
- async def list_all_consumable_stock_logs(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- changeType: Optional[str] = None,
- startTime: Optional[str] = None,
- endTime: Optional[str] = None,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = consumable_stock_log_svc.list_stock_logs(
- db, tenant_no, page=page, page_size=pageSize,
- keyword=keyword, change_type=changeType,
- start_time=startTime, end_time=endTime,
- user_info=user,
- )
- tp = (total + pageSize - 1) // pageSize if total > 0 else 0
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- # ---------------------------------------------------------------------------
- # 2. 试剂
- # ---------------------------------------------------------------------------
- class ReagentCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- reagent_code: str = Field(..., alias="reagentCode")
- reagent_name: str = Field(..., alias="reagentName")
- spec: Optional[str] = None
- standard_usage: Optional[str] = Field(None, alias="standardUsage")
- unit: Optional[str] = None
- description: Optional[str] = None
- warning_qty: Optional[float] = Field(0, alias="warningQty")
- status: Optional[str] = Field("1", alias="status")
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class ReagentUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- reagent_code: Optional[str] = Field(None, alias="reagentCode")
- reagent_name: Optional[str] = Field(None, alias="reagentName")
- spec: Optional[str] = None
- standard_usage: Optional[str] = Field(None, alias="standardUsage")
- unit: Optional[str] = None
- description: Optional[str] = None
- warning_qty: Optional[float] = Field(None, alias="warningQty")
- status: Optional[str] = Field(None, alias="status")
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- @router.get("/reagents")
- async def list_reagents(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:reagent:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = reagent_svc.list_reagents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
- tp = reagent_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/reagents/all")
- async def list_all_reagents(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, _ = reagent_svc.list_reagents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
- return success_response(items)
- @router.post("/reagents")
- async def create_reagent(
- body: ReagentCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- return _legacy_material_write_gone()
- @router.put("/reagents/{reagent_id}")
- async def update_reagent(
- reagent_id: str,
- body: ReagentUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- return _legacy_material_write_gone()
- @router.delete("/reagents/{reagent_id}")
- async def delete_reagent(
- reagent_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- return _legacy_material_write_gone()
- @router.post("/reagents/{reagent_id}/stock-in")
- async def reagent_stock_in(
- reagent_id: str,
- body: StockInBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- from decimal import Decimal
- obj = reagent_svc.stock_in(
- db, reagent_id,
- qty=Decimal(str(body.qty)),
- remark=body.remark,
- user_id=user.get("user_id"),
- tenant_no=tenant_no,
- )
- return success_response(reagent_svc._row_to_dict(obj), message="入库成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/reagents/{reagent_id}/stock-out")
- async def reagent_stock_out(
- reagent_id: str,
- body: StockOutBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- from decimal import Decimal
- obj = reagent_svc.stock_out(
- db, reagent_id,
- qty=Decimal(str(body.qty)),
- receiver=body.receiver,
- purpose=body.purpose,
- remark=body.remark,
- user_id=user.get("user_id"),
- tenant_no=tenant_no,
- )
- return success_response(reagent_svc._row_to_dict(obj), message="出库成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/reagents/batch-stock-in")
- async def reagent_batch_stock_in(
- body: BatchStockInBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump() for it in body.items]
- count = reagent_svc.batch_stock_in(
- db, items=items_data, remark=body.remark,
- tenant_no=tenant_no, user_id=user.get("user_id"),
- )
- return success_response({"count": count}, message=f"成功入库 {count} 条")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/reagents/batch-stock-out")
- async def reagent_batch_stock_out(
- body: BatchStockOutBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump() for it in body.items]
- count = reagent_svc.batch_stock_out(
- db, items=items_data,
- receiver=body.receiver, purpose=body.purpose, remark=body.remark,
- tenant_no=tenant_no, user_id=user.get("user_id"),
- )
- return success_response({"count": count}, message=f"成功出库 {count} 条")
- except ValueError as e:
- return error_response(400, str(e))
- @router.get("/reagents/{reagent_id}/stock-logs")
- async def list_reagent_stock_logs(
- reagent_id: str,
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- changeType: Optional[str] = None,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = reagent_stock_log_svc.list_stock_logs(
- db, tenant_no, page=page, page_size=pageSize,
- material_id=reagent_id, change_type=changeType,
- user_info=user,
- )
- tp = (total + pageSize - 1) // pageSize if total > 0 else 0
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/reagents/stock-logs")
- async def list_all_reagent_stock_logs(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- changeType: Optional[str] = None,
- startTime: Optional[str] = None,
- endTime: Optional[str] = None,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = reagent_stock_log_svc.list_stock_logs(
- db, tenant_no, page=page, page_size=pageSize,
- keyword=keyword, change_type=changeType,
- start_time=startTime, end_time=endTime,
- user_info=user,
- )
- tp = (total + pageSize - 1) // pageSize if total > 0 else 0
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- # ---------------------------------------------------------------------------
- # 3. SOP文档
- # ---------------------------------------------------------------------------
- class SopDocumentCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- sop_code: str = Field(..., alias="sopCode")
- sop_name: str = Field(..., alias="sopName")
- version_no: Optional[str] = Field(None, alias="versionNo")
- sop_attachments: Optional[str] = Field(None, alias="sopAttachments")
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class SopDocumentUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- sop_code: Optional[str] = Field(None, alias="sopCode")
- sop_name: Optional[str] = Field(None, alias="sopName")
- version_no: Optional[str] = Field(None, alias="versionNo")
- sop_attachments: Optional[str] = Field(None, alias="sopAttachments")
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- @router.get("/sop-documents")
- async def list_sop_documents(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:sop-document:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = sop_document_svc.list_sop_documents(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
- tp = sop_document_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/sop-documents/all")
- async def list_all_sop_documents(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, _ = sop_document_svc.list_sop_documents(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
- return success_response(items)
- @router.post("/sop-documents")
- async def create_sop_document(
- body: SopDocumentCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- obj = sop_document_svc.create_sop_document(
- db,
- sop_code=body.sop_code,
- sop_name=body.sop_name,
- version_no=body.version_no,
- sop_attachments=body.sop_attachments,
- tenant_no=tenant_no,
- order_num=body.order_num,
- remark=body.remark,
- user_id=user.get("user_id"),
- )
- return success_response(
- sop_document_svc._row_to_dict(obj, db, tenant_no),
- message="创建成功",
- )
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/sop-documents/{sop_id}")
- async def update_sop_document(
- sop_id: str,
- body: SopDocumentUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
- obj = sop_document_svc.update_sop_document(
- db,
- sop_id,
- sop_code=payload.get("sop_code"),
- sop_name=payload.get("sop_name"),
- version_no=payload.get("version_no"),
- sop_attachments=payload.get("sop_attachments"),
- order_num=payload.get("order_num"),
- remark=payload.get("remark"),
- user_id=user.get("user_id"),
- )
- tenant_no = _get_tenant_no(db, user)
- return success_response(
- sop_document_svc._row_to_dict(obj, db, tenant_no),
- message="更新成功",
- )
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/sop-documents/{sop_id}")
- async def delete_sop_document(
- sop_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- sop_document_svc.soft_delete_sop_document(db, sop_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 4. 工艺工序分类
- # ---------------------------------------------------------------------------
- class ProcessCategoryCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- category_code: str = Field(..., alias="categoryCode")
- category_name: str = Field(..., alias="categoryName")
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class ProcessCategoryUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- category_code: Optional[str] = Field(None, alias="categoryCode")
- category_name: Optional[str] = Field(None, alias="categoryName")
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- @router.get("/process-categories")
- async def list_process_categories(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:process-category:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = process_category_svc.list_process_categories(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
- tp = process_category_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/process-categories/all")
- async def list_all_process_categories(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, _ = process_category_svc.list_process_categories(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
- return success_response(items)
- @router.post("/process-categories")
- async def create_process_category(
- body: ProcessCategoryCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- obj = process_category_svc.create_process_category(
- db,
- category_code=body.category_code,
- category_name=body.category_name,
- tenant_no=tenant_no,
- order_num=body.order_num,
- remark=body.remark,
- user_id=user.get("user_id"),
- )
- return success_response(process_category_svc._row_to_dict(obj), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-categories/{category_id}")
- async def update_process_category(
- category_id: str,
- body: ProcessCategoryUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
- obj = process_category_svc.update_process_category(
- db,
- category_id,
- category_code=payload.get("category_code"),
- category_name=payload.get("category_name"),
- order_num=payload.get("order_num"),
- remark=payload.get("remark"),
- user_id=user.get("user_id"),
- )
- return success_response(process_category_svc._row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/process-categories/{category_id}")
- async def delete_process_category(
- category_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- process_category_svc.soft_delete_process_category(db, category_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 设备管理
- # ---------------------------------------------------------------------------
- class EquipmentCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- equipment_code: str = Field(..., alias="equipmentCode")
- equipment_name: str = Field(..., alias="equipmentName")
- equipment_model: Optional[str] = Field(None, alias="equipmentModel")
- manufacturer: Optional[str] = None
- purchase_date: Optional[str] = Field(None, alias="purchaseDate")
- purchase_amount: Optional[float] = Field(None, alias="purchaseAmount")
- location: Optional[str] = None
- serial_no: Optional[str] = Field(None, alias="serialNo")
- status: Optional[str] = "1"
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class EquipmentUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- equipment_code: Optional[str] = Field(None, alias="equipmentCode")
- equipment_name: Optional[str] = Field(None, alias="equipmentName")
- equipment_model: Optional[str] = Field(None, alias="equipmentModel")
- manufacturer: Optional[str] = None
- purchase_date: Optional[str] = Field(None, alias="purchaseDate")
- purchase_amount: Optional[float] = Field(None, alias="purchaseAmount")
- location: Optional[str] = None
- serial_no: Optional[str] = Field(None, alias="serialNo")
- status: Optional[str] = None
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- @router.get("/equipments")
- async def list_equipments(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- status: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:equipment:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = equipment_svc.list_equipments(
- db,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- status=status,
- tenant_no=tenant_no,
- user_info=user,
- )
- tp = equipment_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/equipments/all")
- async def list_all_equipments(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, _ = equipment_svc.list_equipments(
- db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user
- )
- return success_response(items)
- @router.post("/equipments")
- async def create_equipment(
- body: EquipmentCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- obj = equipment_svc.create_equipment(
- db,
- equipment_code=body.equipment_code,
- equipment_name=body.equipment_name,
- equipment_model=body.equipment_model,
- manufacturer=body.manufacturer,
- purchase_date=body.purchase_date,
- purchase_amount=body.purchase_amount,
- location=body.location,
- serial_no=body.serial_no,
- status=body.status,
- tenant_no=tenant_no,
- order_num=body.order_num,
- remark=body.remark,
- user_id=user.get("user_id"),
- )
- return success_response(equipment_svc._row_to_dict(obj), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/equipments/{equipment_id}")
- async def update_equipment(
- equipment_id: str,
- body: EquipmentUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- payload: dict[str, Any] = body.model_dump(by_alias=False)
- obj = equipment_svc.update_equipment(
- db,
- equipment_id,
- equipment_code=payload.get("equipment_code"),
- equipment_name=payload.get("equipment_name"),
- equipment_model=payload.get("equipment_model"),
- manufacturer=payload.get("manufacturer"),
- purchase_date=payload.get("purchase_date"),
- purchase_amount=payload.get("purchase_amount"),
- location=payload.get("location"),
- serial_no=payload.get("serial_no"),
- status=payload.get("status"),
- order_num=payload.get("order_num"),
- remark=payload.get("remark"),
- user_id=user.get("user_id"),
- )
- return success_response(equipment_svc._row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/equipments/{equipment_id}")
- async def delete_equipment(
- equipment_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- equipment_svc.soft_delete_equipment(db, equipment_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 5. 工艺产出物类型
- # ---------------------------------------------------------------------------
- class OutputTypeCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- type_code: str = Field(..., alias="typeCode")
- type_name: str = Field(..., alias="typeName")
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class OutputTypeUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- type_code: Optional[str] = Field(None, alias="typeCode")
- type_name: Optional[str] = Field(None, alias="typeName")
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- @router.get("/output-types")
- async def list_output_types(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:output-type:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = output_type_svc.list_output_types(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
- tp = output_type_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/output-types/all")
- async def list_all_output_types(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, _ = output_type_svc.list_output_types(db, page=1, page_size=9999, tenant_no=tenant_no, user_info=user)
- return success_response(items)
- @router.post("/output-types")
- async def create_output_type(
- body: OutputTypeCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- obj = output_type_svc.create_output_type(
- db,
- type_code=body.type_code,
- type_name=body.type_name,
- tenant_no=tenant_no,
- order_num=body.order_num,
- remark=body.remark,
- user_id=user.get("user_id"),
- )
- return success_response(output_type_svc._row_to_dict(obj), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/output-types/{type_id}")
- async def update_output_type(
- type_id: str,
- body: OutputTypeUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
- obj = output_type_svc.update_output_type(
- db,
- type_id,
- type_code=payload.get("type_code"),
- type_name=payload.get("type_name"),
- order_num=payload.get("order_num"),
- remark=payload.get("remark"),
- user_id=user.get("user_id"),
- )
- return success_response(output_type_svc._row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/output-types/{type_id}")
- async def delete_output_type(
- type_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- output_type_svc.soft_delete_output_type(db, type_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 5b. 生产产品类型(生产专用,解耦中试 biz_cell_product_type)
- # ---------------------------------------------------------------------------
- class ProductionProductTypeCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- product_type_code: str = Field(..., alias="productTypeCode")
- product_type_name: str = Field(..., alias="productTypeName")
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class ProductionProductTypeUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- product_type_code: Optional[str] = Field(None, alias="productTypeCode")
- product_type_name: Optional[str] = Field(None, alias="productTypeName")
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- @router.get("/production-product-types")
- async def list_production_product_types(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = production_product_type_svc.list_production_product_types(
- db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user
- )
- tp = production_product_type_svc.total_pages(total, pageSize)
- return success_response(
- {"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}
- )
- @router.post("/production-product-types")
- async def create_production_product_type(
- body: ProductionProductTypeCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- obj = production_product_type_svc.create_production_product_type(
- db,
- product_type_code=body.product_type_code,
- product_type_name=body.product_type_name,
- tenant_no=tenant_no,
- order_num=body.order_num,
- remark=body.remark,
- user_id=user.get("user_id"),
- )
- return success_response(production_product_type_svc._row_to_dict(obj), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-product-types/{row_id}")
- async def update_production_product_type(
- row_id: str,
- body: ProductionProductTypeUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
- obj = production_product_type_svc.update_production_product_type(
- db,
- row_id,
- product_type_code=payload.get("product_type_code"),
- product_type_name=payload.get("product_type_name"),
- order_num=payload.get("order_num"),
- remark=payload.get("remark"),
- user_id=user.get("user_id"),
- tenant_no=tenant_no,
- )
- return success_response(production_product_type_svc._row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/production-product-types/{row_id}")
- async def delete_production_product_type(
- row_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- production_product_type_svc.soft_delete_production_product_type(
- db, row_id, tenant_no=tenant_no
- )
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 6. 耗材包(主 + 子项)
- # ---------------------------------------------------------------------------
- class PackItemInput(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- consumable_code: Optional[str] = Field(None, alias="consumableCode")
- consumable_name: Optional[str] = Field(None, alias="consumableName")
- spec_model: Optional[str] = Field(None, alias="specModel")
- standard_usage: Optional[str] = Field(None, alias="standardUsage")
- unit: Optional[str] = None
- remark: Optional[str] = None
- class ConsumablePackCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- pack_code: str = Field(..., alias="packCode")
- pack_name: str = Field(..., alias="packName")
- usage_scene: Optional[str] = Field(None, alias="usageScene")
- pack_type: Optional[str] = Field(None, alias="packType")
- status: Optional[int] = 1
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- items: List[PackItemInput] = []
- class ConsumablePackUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- pack_code: Optional[str] = Field(None, alias="packCode")
- pack_name: Optional[str] = Field(None, alias="packName")
- usage_scene: Optional[str] = Field(None, alias="usageScene")
- pack_type: Optional[str] = Field(None, alias="packType")
- status: Optional[int] = None
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- items: Optional[List[PackItemInput]] = None
- @router.get("/consumable-packs")
- async def list_consumable_packs(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:consumable-pack:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = consumable_pack_svc.list_consumable_packs(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
- tp = consumable_pack_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/consumable-packs/all")
- async def list_all_consumable_packs(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items = consumable_pack_svc.list_all_packs(db, tenant_no, user_info=user)
- return success_response(items)
- @router.get("/consumable-packs/{pack_id}")
- async def get_consumable_pack_detail(
- pack_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- detail = consumable_pack_svc.get_pack_detail(db, pack_id)
- if not detail:
- return error_response(404, "耗材包不存在")
- return success_response(detail)
- @router.post("/consumable-packs")
- async def create_consumable_pack(
- body: ConsumablePackCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items else None
- obj = consumable_pack_svc.create_consumable_pack(
- db,
- pack_code=body.pack_code,
- pack_name=body.pack_name,
- usage_scene=body.usage_scene,
- pack_type=body.pack_type,
- status=body.status,
- items=items_data,
- tenant_no=tenant_no,
- order_num=body.order_num,
- remark=body.remark,
- user_id=user.get("user_id"),
- )
- return success_response(consumable_pack_svc._row_to_dict(obj), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/consumable-packs/{pack_id}")
- async def update_consumable_pack(
- pack_id: str,
- body: ConsumablePackUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
- items_data = [it.model_dump(by_alias=False) for it in body.items] if body.items is not None else None
- obj = consumable_pack_svc.update_consumable_pack(
- db,
- pack_id,
- pack_code=payload.get("pack_code"),
- pack_name=payload.get("pack_name"),
- usage_scene=payload.get("usage_scene"),
- pack_type=payload.get("pack_type"),
- status=payload.get("status"),
- items=items_data,
- order_num=payload.get("order_num"),
- remark=payload.get("remark"),
- user_id=user.get("user_id"),
- )
- return success_response(consumable_pack_svc._row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/consumable-packs/{pack_id}")
- async def delete_consumable_pack(
- pack_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- consumable_pack_svc.soft_delete_consumable_pack(db, pack_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 7. 工艺库(主 + 3 个子表:试剂、耗材、产出物)
- # ---------------------------------------------------------------------------
- class ProcessReagentInput(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- reagent_code: Optional[str] = Field(None, alias="reagentCode")
- reagent_name: Optional[str] = Field(None, alias="reagentName")
- spec: Optional[str] = None
- standard_usage: Optional[str] = Field(None, alias="standardUsage")
- unit: Optional[str] = None
- is_key_material: Optional[str] = Field("0", alias="isKeyMaterial")
- remark: Optional[str] = None
- class ProcessConsumableInput(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- consumable_code: Optional[str] = Field(None, alias="consumableCode")
- consumable_name: Optional[str] = Field(None, alias="consumableName")
- spec_model: Optional[str] = Field(None, alias="specModel")
- standard_usage: Optional[str] = Field(None, alias="standardUsage")
- unit: Optional[str] = None
- is_required: Optional[str] = Field("1", alias="isRequired")
- remark: Optional[str] = None
- class ProcessOutputInput(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- output_type_id: Optional[str] = Field(None, alias="outputTypeId")
- output_name: Optional[str] = Field(None, alias="outputName")
- output_barcode: Optional[str] = Field(None, alias="outputBarcode")
- remark: Optional[str] = None
- class ProcessDataCollectionInput(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- collection_name: str = Field(..., alias="collectionName")
- unit: Optional[str] = None
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- class ProcessCreateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- process_code: str = Field(..., alias="processCode")
- category_id: Optional[str] = Field(None, alias="categoryId")
- process_name: str = Field(..., alias="processName")
- description: Optional[str] = None
- equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds")
- estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
- sop_ids: Optional[str] = Field(None, alias="sopIds")
- sop_content: Optional[str] = Field(None, alias="sopContent")
- order_num: Optional[int] = Field(0, alias="orderNum")
- remark: Optional[str] = None
- reagents: List[ProcessReagentInput] = []
- consumables: List[ProcessConsumableInput] = []
- outputs: List[ProcessOutputInput] = []
- data_collections: List[ProcessDataCollectionInput] = Field([], alias="dataCollections")
- class ProcessUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- process_code: Optional[str] = Field(None, alias="processCode")
- category_id: Optional[str] = Field(None, alias="categoryId")
- process_name: Optional[str] = Field(None, alias="processName")
- description: Optional[str] = None
- equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds")
- estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
- sop_ids: Optional[str] = Field(None, alias="sopIds")
- sop_content: Optional[str] = Field(None, alias="sopContent")
- order_num: Optional[int] = Field(None, alias="orderNum")
- remark: Optional[str] = None
- reagents: Optional[List[ProcessReagentInput]] = None
- consumables: Optional[List[ProcessConsumableInput]] = None
- outputs: Optional[List[ProcessOutputInput]] = None
- data_collections: Optional[List[ProcessDataCollectionInput]] = Field(None, alias="dataCollections")
- @router.get("/processes")
- async def list_processes(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:process:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = process_svc.list_processes(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
- tp = process_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/processes/all")
- async def list_all_processes(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items = process_svc.list_all_processes(db, tenant_no, user_info=user)
- return success_response(items)
- @router.get("/processes/{process_id}")
- async def get_process_detail(
- process_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- detail = process_svc.get_process_detail(db, process_id)
- if not detail:
- return error_response(404, "工艺不存在")
- return success_response(detail)
- @router.post("/processes")
- async def create_process(
- body: ProcessCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents else None
- consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables else None
- outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs else None
- data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections else None
- # Convert string fields to appropriate types for sub-records
- for rd in (reagents_data or []):
- if "is_key_material" in rd and isinstance(rd["is_key_material"], str):
- rd["is_key_material"] = int(rd["is_key_material"])
- for cd in (consumables_data or []):
- if "is_required" in cd and isinstance(cd["is_required"], str):
- cd["is_required"] = int(cd["is_required"])
- obj = process_svc.create_process(
- db,
- process_code=body.process_code,
- category_id=body.category_id,
- process_name=body.process_name,
- description=body.description,
- equipment_ids=body.equipment_ids,
- estimated_duration=body.estimated_duration,
- sop_ids=body.sop_ids,
- sop_content=body.sop_content,
- reagents=reagents_data,
- consumables=consumables_data,
- outputs=outputs_data,
- data_collections=data_collections_data,
- tenant_no=tenant_no,
- order_num=body.order_num,
- remark=body.remark,
- user_id=user.get("user_id"),
- )
- return success_response(process_svc._row_to_dict(obj), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/processes/{process_id}")
- async def update_process(
- process_id: str,
- body: ProcessUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
- reagents_data = [r.model_dump(by_alias=False) for r in body.reagents] if body.reagents is not None else None
- consumables_data = [c.model_dump(by_alias=False) for c in body.consumables] if body.consumables is not None else None
- outputs_data = [o.model_dump(by_alias=False) for o in body.outputs] if body.outputs is not None else None
- data_collections_data = [d.model_dump(by_alias=False) for d in body.data_collections] if body.data_collections is not None else None
- # Convert string fields to appropriate types for sub-records
- for rd in (reagents_data or []):
- if "is_key_material" in rd and isinstance(rd["is_key_material"], str):
- rd["is_key_material"] = int(rd["is_key_material"])
- for cd in (consumables_data or []):
- if "is_required" in cd and isinstance(cd["is_required"], str):
- cd["is_required"] = int(cd["is_required"])
- update_kwargs: dict[str, Any] = {
- "process_code": payload.get("process_code"),
- "category_id": payload.get("category_id"),
- "process_name": payload.get("process_name"),
- "description": payload.get("description"),
- "estimated_duration": payload.get("estimated_duration"),
- "sop_ids": payload.get("sop_ids"),
- "sop_content": payload.get("sop_content"),
- "reagents": reagents_data,
- "consumables": consumables_data,
- "outputs": outputs_data,
- "data_collections": data_collections_data,
- "order_num": payload.get("order_num"),
- "remark": payload.get("remark"),
- "user_id": user.get("user_id"),
- }
- if "equipment_ids" in payload:
- update_kwargs["equipment_ids"] = payload.get("equipment_ids")
- obj = process_svc.update_process(db, process_id, **update_kwargs)
- return success_response(process_svc._row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/processes/{process_id}")
- async def delete_process(
- process_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- process_svc.soft_delete_process(db, process_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 8. 工艺路线管理
- # ---------------------------------------------------------------------------
- @router.get("/process-routes")
- async def list_process_routes(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- routeName: Optional[str] = None,
- routeCode: Optional[str] = None,
- productTypeId: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:process-route:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = route_svc.list_routes(
- db, page=page, page_size=pageSize,
- route_name=routeName, route_code=routeCode,
- product_type_id=productTypeId, tenant_no=tenant_no,
- user_info=user,
- )
- tp = route_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/process-routes/{route_id}")
- async def get_process_route_detail(
- route_id: str,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- detail = route_svc.get_route_detail(db, route_id)
- if not detail:
- return error_response(404, "路线不存在")
- return success_response(detail)
- @router.post("/process-routes")
- async def create_process_route(
- body: ProcessRouteCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- obj = route_svc.create_route(
- db,
- route_code=body.route_code,
- route_name=body.route_name,
- product_type_id=body.product_type_id,
- version=body.version,
- remark=body.remark,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- return success_response(route_svc._route_row_to_dict(obj), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-routes/{route_id}")
- async def update_process_route(
- route_id: str,
- body: ProcessRouteUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_route_writable(db, route_id, user)
- payload = body.model_dump(exclude_unset=True, by_alias=False)
- update_kwargs = {
- "user_id": user.get("user_id"),
- "route_code": payload.get("route_code"),
- "route_name": payload.get("route_name"),
- "product_type_id": payload.get("product_type_id"),
- "version": payload.get("version"),
- "remark": payload.get("remark"),
- }
- if "equipment_ids" in payload:
- update_kwargs["equipment_ids"] = payload.get("equipment_ids")
- obj = route_svc.update_route(db, route_id, **update_kwargs)
- return success_response(route_svc._route_row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-routes/set-current")
- async def set_current_process_route(
- body: SetCurrentBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_route_writable(db, body.route_id, user)
- tenant_no = _get_tenant_no(db, user)
- obj = route_svc.set_current_route(db, body.route_id, tenant_no)
- return success_response(route_svc._route_row_to_dict(obj), message="设置成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/process-routes/{route_id}")
- async def delete_process_route(
- route_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_route_writable(db, route_id, user)
- route_svc.soft_delete_route(db, route_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ----- 步骤操作 -----
- @router.post("/process-routes/step/add")
- async def add_route_step(
- body: StepAddBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_route_writable(db, body.route_id, user)
- tenant_no = _get_tenant_no(db, user)
- step_data = route_svc.add_step(
- db,
- route_id=body.route_id,
- process_id=body.process_id,
- step_order=body.step_order,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- return success_response(step_data, message="添加成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/process-routes/step/copy")
- async def copy_route_step(
- body: StepCopyBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_step_route_writable(db, body.step_id, user)
- tenant_no = _get_tenant_no(db, user)
- step_data = route_svc.copy_step(
- db,
- step_id=body.step_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- return success_response(step_data, message="复制成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-routes/step/update")
- async def update_route_step(
- body: StepUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_step_route_writable(db, body.step_id, user)
- payload = body.model_dump(exclude_unset=True, by_alias=False)
- payload.pop("step_id", None)
- obj = route_svc.update_step(db, body.step_id, user_id=user.get("user_id"), **payload)
- return success_response(route_svc._step_row_to_dict(obj), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-routes/step/reorder")
- async def reorder_route_steps(
- body: StepReorderBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_route_writable(db, body.route_id, user)
- route_svc.reorder_steps(db, body.route_id, body.step_ids)
- return success_response(None, message="排序成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/process-routes/step/{step_id}")
- async def delete_route_step(
- step_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_step_route_writable(db, step_id, user)
- route_svc.soft_delete_step(db, step_id)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ----- 步骤子表 -----
- @router.put("/process-routes/step/reagent/update")
- async def update_step_reagents(
- body: StepReagentUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_step_route_writable(db, body.step_id, user)
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body.items]
- route_svc.update_step_reagents(db, body.step_id, items_data, tenant_no, user.get("user_id"))
- return success_response(None, message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-routes/step/consumable/update")
- async def update_step_consumables(
- body: StepConsumableUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_step_route_writable(db, body.step_id, user)
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body.items]
- route_svc.update_step_consumables(db, body.step_id, items_data, tenant_no, user.get("user_id"))
- return success_response(None, message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-routes/step/output/update")
- async def update_step_outputs(
- body: StepOutputUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_step_route_writable(db, body.step_id, user)
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body.items]
- route_svc.update_step_outputs(db, body.step_id, items_data, tenant_no, user.get("user_id"))
- return success_response(None, message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/process-routes/step/data-collection/update")
- async def update_step_data_collections(
- body: StepDataCollectionUpdateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- route_svc.assert_step_route_writable(db, body.step_id, user)
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body.items]
- route_svc.update_step_data_collections(db, body.step_id, items_data, tenant_no, user.get("user_id"))
- return success_response(None, message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 9. 入库生产工单
- # ---------------------------------------------------------------------------
- @router.post("/production-work-orders")
- async def create_production_work_order(
- body: ProductionWorkOrderCreate,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- wo_type = (body.work_order_type or "inbound").strip()
- if wo_type == "temporary":
- wo = production_work_order_svc.create_temp_production_work_order(
- db,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- remark=body.remark or "",
- )
- else:
- wo = production_work_order_svc.create_production_work_order_manual(
- db,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- protocol_no=body.protocol_no or "",
- sample_code=body.sample_code or "",
- reception_source=body.reception_source or "cell_storage",
- remark=body.remark or "",
- )
- db.commit()
- return success_response(production_work_order_svc._wo_to_dict(wo), message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- db.rollback()
- return error_response(400, str(e))
- @router.get("/production-work-orders")
- async def list_production_work_orders(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- workOrderNo: Optional[str] = None,
- workOrderStatus: Optional[str] = Query(None, description="工单状态,多个用英文逗号分隔"),
- workOrderType: Optional[str] = None,
- receptionSource: Optional[str] = Query(None, alias="receptionSource"),
- bizType: Optional[str] = Query(None, alias="bizType"),
- protocolNo: Optional[str] = None,
- sampleCode: Optional[str] = None,
- createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
- createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
- infusionTimeStart: Optional[str] = Query(None, alias="infusionTimeStart"),
- infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
- plannedStartTimeStart: Optional[str] = Query(None, alias="plannedStartTimeStart"),
- plannedStartTimeEnd: Optional[str] = Query(None, alias="plannedStartTimeEnd"),
- excludeVoidProtocol: bool = Query(True, alias="excludeVoidProtocol"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- query = ProductionWorkOrderListQuery(
- page=page, page_size=pageSize,
- keyword=keyword,
- work_order_no=workOrderNo, work_order_status=workOrderStatus,
- work_order_type=workOrderType,
- reception_source=receptionSource,
- biz_type=bizType,
- protocol_no=protocolNo, sample_code=sampleCode,
- create_time_start=createTimeStart,
- create_time_end=createTimeEnd,
- infusion_time_start=infusionTimeStart,
- infusion_time_end=infusionTimeEnd,
- planned_start_time_start=plannedStartTimeStart,
- planned_start_time_end=plannedStartTimeEnd,
- exclude_void_protocol=excludeVoidProtocol,
- )
- result = production_work_order_svc.list_production_work_orders(
- db, tenant_no=tenant_no, query=query, page=page, page_size=pageSize,
- user_info=user,
- )
- items = result.get("items", [])
- total = result.get("total", 0)
- tp = production_task_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/production-work-orders/tab-step-counts")
- async def production_work_order_tab_step_counts(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- counts = production_task_svc.count_production_work_order_tab_step_badges(
- db, tenant_no=tenant_no, user_info=user
- )
- return success_response(counts)
- except PermissionError as e:
- return error_response(403, str(e))
- except Exception as e:
- return error_response(400, str(e))
- @router.get("/production-work-orders/mine-tab-step-counts")
- async def production_work_order_mine_tab_step_counts(
- user: dict = Depends(require_menu_code("production:my-task:list")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- counts = production_task_svc.count_mine_production_work_order_tab_step_badges(
- db,
- tenant_no=tenant_no,
- assignee_user_id=_get_current_user_id(user),
- )
- return success_response(counts)
- except PermissionError as e:
- return error_response(403, str(e))
- except Exception as e:
- return error_response(400, str(e))
- @router.get("/production-work-orders/{wo_id}")
- async def get_production_work_order(
- wo_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- wo = production_work_order_svc.get_production_work_order(db, wo_id, tenant_no)
- if not wo:
- return error_response(404, "工单不存在")
- return success_response(production_work_order_svc._wo_to_response_dict(db, wo))
- @router.put("/production-work-orders/{wo_id}")
- async def update_production_work_order_api(
- wo_id: str,
- body: ProductionWorkOrderUpdate,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- wo = production_work_order_svc.update_production_work_order(
- db,
- wo_id,
- tenant_no,
- user.get("user_id"),
- remark=body.remark,
- )
- db.commit()
- return success_response(
- production_work_order_svc._wo_to_response_dict(db, wo),
- message="保存成功",
- )
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- except Exception as e:
- db.rollback()
- return error_response(400, str(e))
- @router.put("/production-work-orders/{wo_id}/complete")
- async def complete_production_work_order(
- wo_id: str,
- body: ProductionWorkOrderComplete,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- wo = production_work_order_svc.complete_production_work_order(
- db, wo_id, tenant_no, user.get("user_id"), body,
- )
- return success_response(production_work_order_svc._wo_to_response_dict(db, wo), message="操作成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-work-orders/{wo_id}/void")
- async def void_production_work_order_api(
- wo_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- wo = production_work_order_svc.void_production_work_order(
- db, wo_id, tenant_no, user.get("user_id"),
- )
- return success_response(
- production_work_order_svc._wo_to_response_dict(db, wo),
- message="工单已作废",
- )
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/production-work-orders/{wo_id}")
- async def delete_production_work_order_api(
- wo_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- production_work_order_svc.delete_production_work_order(db, wo_id, tenant_no, user.get("user_id"))
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/production-work-orders/{wo_id}/configure")
- async def configure_production_work_order(
- wo_id: str,
- body: ProductionWorkOrderConfigureBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- wo = production_work_order_svc.configure_temp_production_work_order(
- db,
- wo_id,
- task_name=body.task_name or "",
- task_type=body.task_type or "cell_storage",
- route_id=body.route_id,
- description=body.description or "",
- remark=body.remark or "",
- assignee_user_id=body.assignee_user_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- )
- db.commit()
- return success_response(production_work_order_svc._wo_to_dict(wo), message="配置成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- except Exception as e:
- db.rollback()
- return error_response(400, str(e))
- @router.get("/production-work-orders/{wo_id}/cell-product-options")
- async def list_production_work_order_cell_product_options(
- wo_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items = qc_work_order_svc.list_step_output_dispatch_options_for_production_work_order(
- db, production_work_order_id=wo_id, tenant_no=tenant_no
- )
- return success_response(items)
- except ValueError as e:
- return error_response(400, str(e))
- @router.get("/production-work-orders/{wo_id}/qc-info")
- async def get_production_work_order_qc_info_api(
- wo_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- data = qc_work_order_svc.get_production_work_order_qc_info(
- db, production_work_order_id=wo_id, tenant_no=tenant_no
- )
- return success_response(data)
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/production-work-orders/{wo_id}/qc-work-orders")
- async def create_qc_work_order_from_production_api(
- wo_id: str,
- body: ProductionQcWorkOrderCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- wo = qc_work_order_svc.create_qc_work_order_from_production(
- db,
- production_work_order_id=wo_id,
- production_step_output_id=body.production_step_output_id,
- remark=body.remark or "",
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- )
- db.commit()
- return success_response(qc_work_order_svc._wo_to_dict(wo), message="派发成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- except Exception as e:
- db.rollback()
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 生产任务集合
- # ---------------------------------------------------------------------------
- @router.get("/production-task-sets")
- async def list_production_task_sets(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- status: Optional[str] = None,
- user: dict = Depends(require_menu_code("production:task-set:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = production_task_set_svc.list_sets(
- db, tenant_no=tenant_no, page=page, page_size=pageSize, keyword=keyword, status=status,
- )
- tp = production_task_set_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/production-task-sets/active")
- async def list_active_production_task_sets(
- user: dict = Depends(require_any_menu_code("production:task-set:list", "production:work-order:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items = production_task_set_svc.list_active_sets(db, tenant_no=tenant_no)
- return success_response({"list": items})
- @router.post("/production-task-sets/from-work-order")
- async def create_production_task_set_from_work_order(
- body: ProductionTaskSetFromWorkOrderBody,
- user: dict = Depends(
- require_any_menu_code("production:work-order:list", "production:task-set:add")
- ),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- detail = production_task_set_svc.create_set_from_work_order(
- db,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- work_order_id=body.work_order_id,
- set_name=body.set_name,
- remark=body.remark,
- items=(
- [x.model_dump(by_alias=False) for x in body.items]
- if body.items is not None
- else None
- ),
- )
- db.commit()
- return success_response(detail, message="保存成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.get("/production-task-sets/{set_id}")
- async def get_production_task_set(
- set_id: str,
- user: dict = Depends(require_any_menu_code("production:task-set:list", "production:work-order:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- detail = production_task_set_svc.get_set(db, set_id, tenant_no)
- if not detail:
- return error_response(404, "集合不存在")
- return success_response(detail)
- @router.post("/production-task-sets")
- async def create_production_task_set(
- body: ProductionTaskSetCreateBody,
- user: dict = Depends(require_menu_code("production:task-set:add")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- detail = production_task_set_svc.create_set(
- db,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- set_name=body.set_name,
- status=body.status or "1",
- remark=body.remark,
- items=[x.model_dump(by_alias=False) for x in body.items],
- )
- db.commit()
- return success_response(detail, message="创建成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.put("/production-task-sets/{set_id}")
- async def update_production_task_set(
- set_id: str,
- body: ProductionTaskSetUpdateBody,
- user: dict = Depends(require_menu_code("production:task-set:edit")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- production_task_set_svc.assert_set_writable(db, set_id, tenant_no, user)
- data = body.model_dump(by_alias=False, exclude_unset=True)
- items = None
- if "items" in data:
- items = [x.model_dump(by_alias=False) for x in (body.items or [])]
- detail = production_task_set_svc.update_set(
- db,
- set_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- set_name=data.get("set_name"),
- status=data.get("status"),
- remark=data.get("remark"),
- items=items,
- )
- db.commit()
- return success_response(detail, message="更新成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.delete("/production-task-sets/{set_id}")
- async def delete_production_task_set(
- set_id: str,
- user: dict = Depends(require_menu_code("production:task-set:remove")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- production_task_set_svc.assert_set_writable(db, set_id, tenant_no, user)
- production_task_set_svc.delete_set(
- db, set_id, tenant_no=tenant_no, user_id=user.get("user_id")
- )
- db.commit()
- return success_response(None, message="删除成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 10. 生产任务
- # ---------------------------------------------------------------------------
- @router.post("/production-tasks")
- async def create_production_task(
- body: ProductionTaskCreate,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- data = body.model_dump(by_alias=False)
- task = production_task_svc.create_production_task(
- db, tenant_no=tenant_no, user_id=user.get("user_id"),
- create_dept_id="", data=data,
- )
- db.commit()
- result = production_task_svc._task_to_dict(task)
- if data.get("route_id"):
- production_task_svc._enrich_tasks_with_step_summaries(db, [result], tenant_no=tenant_no)
- return success_response(result, message="创建成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/production-tasks/from-set")
- async def create_production_tasks_from_set(
- body: ProductionTasksFromSetBody,
- user: dict = Depends(require_menu_code("production:work-order:list")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- result = production_task_set_svc.create_tasks_from_set(
- db,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id="",
- work_order_id=body.work_order_id,
- set_id=body.set_id,
- items=(
- [x.model_dump(by_alias=False) for x in body.items]
- if body.items is not None
- else None
- ),
- )
- db.commit()
- return success_response(result, message="创建成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.get("/production-tasks")
- async def list_production_tasks(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- workOrderId: Optional[str] = None,
- taskNo: Optional[str] = None,
- taskStatus: Optional[str] = None,
- startedTimeStart: Optional[str] = Query(None, alias="startedTimeStart"),
- startedTimeEnd: Optional[str] = Query(None, alias="startedTimeEnd"),
- includeStepSummary: bool = Query(False, alias="includeStepSummary"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = production_task_svc.list_production_tasks(
- db, tenant_no=tenant_no, page=page, page_size=pageSize,
- keyword=keyword,
- work_order_id=workOrderId, task_no=taskNo, task_status=taskStatus,
- started_time_start=startedTimeStart,
- started_time_end=startedTimeEnd,
- user_info=user,
- include_step_summary=includeStepSummary,
- )
- tp = production_task_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/production-waste-outputs")
- async def list_production_waste_outputs(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
- createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = production_waste_output_svc.list_production_waste_outputs(
- db,
- tenant_no=tenant_no,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- create_time_start=createTimeStart,
- create_time_end=createTimeEnd,
- user_info=user,
- )
- tp = production_waste_output_svc.total_pages(total, pageSize)
- return success_response(
- {"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}
- )
- @router.post("/production-tasks/list-by-work-orders")
- async def list_production_tasks_by_work_orders(
- body: ProductionTaskListByWorkOrdersBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- wo_ids = [x.strip() for x in body.work_order_ids if x and str(x).strip()]
- if not wo_ids:
- return success_response(
- {"list": [], "total": 0, "page": 1, "pageSize": body.page_size, "totalPages": 0}
- )
- effective_page_size = min(max(body.page_size, len(wo_ids) * 100), 500)
- items, total = production_task_svc.list_production_tasks(
- db,
- tenant_no=tenant_no,
- page=1,
- page_size=effective_page_size,
- work_order_ids=wo_ids,
- user_info=user,
- include_step_summary=body.include_step_summary,
- )
- tp = production_task_svc.total_pages(total, effective_page_size)
- return success_response(
- {
- "list": items,
- "total": total,
- "page": 1,
- "pageSize": effective_page_size,
- "totalPages": tp,
- }
- )
- @router.get("/production-tasks/mine")
- async def list_my_production_tasks(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- taskStatus: Optional[str] = None,
- workOrderType: Optional[str] = None,
- receptionSource: Optional[str] = Query(None, alias="receptionSource"),
- bizType: Optional[str] = Query(None, alias="bizType"),
- scope: str = Query("current", description="current|history"),
- includeStepSummary: bool = Query(False, alias="includeStepSummary"),
- excludeVoidProtocol: bool = Query(True, alias="excludeVoidProtocol"),
- plannedStartTimeStart: Optional[str] = Query(None, alias="plannedStartTimeStart"),
- plannedStartTimeEnd: Optional[str] = Query(None, alias="plannedStartTimeEnd"),
- user: dict = Depends(require_menu_code("production:my-task:list")),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- items, total = production_task_svc.list_mine_production_tasks(
- db,
- assignee_user_id=_get_current_user_id(user),
- tenant_no=tenant_no,
- scope=scope,
- page=page,
- page_size=pageSize,
- keyword=keyword,
- task_status=taskStatus,
- work_order_type=workOrderType,
- reception_source=receptionSource,
- biz_type=bizType,
- include_step_summary=includeStepSummary,
- exclude_void_protocol=excludeVoidProtocol,
- planned_start_time_start=plannedStartTimeStart,
- planned_start_time_end=plannedStartTimeEnd,
- )
- tp = production_task_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.post("/production-tasks/{task_id}/forward")
- async def forward_production_task_api(
- task_id: str,
- body: ProductionTaskForwardBody,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_any_menu_code("production:my-task:list", "production:work-order:list")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- scope = (operateScope or "mine").strip().lower()
- require_assignee = scope == "mine"
- from_uid = _get_current_user_id(user) if require_assignee else (user.get("user_id") or "")
- task = production_task_svc.forward_production_task(
- db,
- task_id,
- from_user_id=from_uid,
- to_user_id=body.to_user_id,
- tenant_no=tenant_no,
- operator_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- remark=body.remark,
- require_assignee=require_assignee,
- )
- db.commit()
- item = production_task_svc._task_to_dict(task)
- production_task_svc._enrich_assignee_names(db, [item])
- return success_response(item, message="转发成功")
- except PermissionError as e:
- db.rollback()
- return error_response(403, str(e))
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.post("/production-tasks/{task_id}/spawn-temporary-work-order")
- async def spawn_temporary_work_order_from_task_api(
- task_id: str,
- body: SpawnTemporaryWorkOrderBody,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(
- require_any_menu_code("production:my-task:list", "production:work-order:list")
- ),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- result = production_task_svc.spawn_temporary_work_order_from_task(
- db,
- task_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- remark=body.remark or "",
- )
- db.commit()
- return success_response(result, message="临时工单已创建")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/assignee")
- async def reassign_production_task_api(
- task_id: str,
- body: ProductionTaskAssigneeUpdate,
- user: dict = Depends(require_menu_code("production:work-order:list")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- task = production_task_svc.reassign_production_task(
- db,
- task_id,
- assignee_user_id=body.assignee_user_id,
- tenant_no=tenant_no,
- operator_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- remark=body.remark,
- )
- db.commit()
- item = production_task_svc._task_to_dict(task)
- production_task_svc._enrich_assignee_names(db, [item])
- return success_response(item, message="改派成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/steps/dispatch")
- async def dispatch_production_task_steps_api(
- task_id: str,
- body: ProductionTaskStepDispatchBody,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_any_menu_code("production:my-task:list", "production:work-order:list")),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- payload = [
- {
- "step_id": it.step_id,
- "assignee_user_ids": it.assignee_user_ids,
- "planned_start_time": it.planned_start_time,
- }
- for it in body.steps
- ]
- detail = production_task_svc.dispatch_task_steps(
- db,
- task_id,
- tenant_no=tenant_no,
- steps_payload=payload,
- operator_id=user.get("user_id"),
- create_dept_id=user.get("dept_id", ""),
- )
- db.commit()
- return success_response(detail, message="步骤派发成功")
- except ValueError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.get("/production-tasks/{task_id}/editor-init")
- async def get_production_task_editor_init(
- task_id: str,
- includeReference: bool = Query(True, alias="includeReference"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """编辑生产任务弹窗初始化:任务详情 + 工艺库 + 步骤配置参考数据(聚合接口)。"""
- tenant_no = _get_tenant_no(db, user)
- payload = production_task_svc.get_production_task_editor_init(
- db,
- task_id,
- tenant_no,
- user_info=user,
- include_reference=includeReference,
- )
- if not payload:
- return error_response(404, "任务不存在")
- return success_response(payload)
- @router.get("/production-tasks/{task_id}")
- async def get_production_task_detail(
- task_id: str,
- includeStepSubTables: bool = Query(True, alias="includeStepSubTables"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- detail = production_task_svc.get_production_task_detail(
- db,
- task_id,
- tenant_no,
- include_step_sub_tables=includeStepSubTables,
- )
- if not detail:
- return error_response(404, "任务不存在")
- return success_response(detail)
- @router.put("/production-tasks/{task_id}")
- async def update_production_task(
- task_id: str,
- body: ProductionTaskUpdate,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- data = body.model_dump(exclude_unset=True, by_alias=False)
- task = production_task_svc.update_production_task(
- db, task_id, tenant_no, user.get("user_id"), data,
- )
- return success_response(production_task_svc._task_to_dict(task), message="更新成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/production-tasks/{task_id}")
- async def delete_production_task(
- task_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- production_task_svc.delete_production_task(db, task_id, tenant_no, user.get("user_id"))
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/status")
- async def update_production_task_status(
- task_id: str,
- body: ProductionTaskStatusUpdate,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- task = production_task_svc.update_task_status(
- db,
- task_id,
- tenant_no,
- user.get("user_id"),
- body.task_status,
- conclusion=body.conclusion,
- remark=body.remark,
- )
- return success_response(production_task_svc._task_to_dict(task), message="操作成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/production-tasks/{task_id}/steps")
- async def add_production_task_step(
- task_id: str,
- body: TaskStepAdd,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- data = body.model_dump(by_alias=False)
- step_data = production_task_svc.add_task_step(
- db, task_id, tenant_no, user.get("user_id"), data,
- )
- return success_response(step_data, message="添加成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/steps/{step_id}")
- async def update_production_task_step(
- task_id: str,
- step_id: str,
- body: TaskStepUpdate,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- data = body.model_dump(exclude_unset=True, by_alias=False)
- step = production_task_svc.update_task_step(
- db, step_id, tenant_no, user.get("user_id"), data,
- )
- return success_response(production_task_svc._step_to_dict(step), message="更新成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/production-tasks/{task_id}/steps/{step_id}")
- async def delete_production_task_step(
- task_id: str,
- step_id: str,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- production_task_svc.delete_task_step(db, step_id, tenant_no, user.get("user_id"))
- return success_response(None, message="删除成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/step/reorder")
- async def reorder_production_task_steps(
- body: TaskStepReorderBody,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- production_task_svc.reorder_task_steps(db, body.task_id, body.step_ids)
- return success_response(None, message="排序成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/production-tasks/{task_id}/steps/{step_id}/copy")
- async def copy_production_task_step(
- task_id: str,
- step_id: str,
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- step_data = production_task_svc.copy_task_step(
- db,
- step_id=step_id,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- )
- return success_response(step_data, message="复制成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/steps/{step_id}/reagents")
- async def replace_task_step_reagents(
- task_id: str,
- step_id: str,
- body: List[TaskStepMaterialItem],
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = []
- for it in body:
- d = it.model_dump(by_alias=False)
- items_data.append({
- "reagent_code": d.get("material_code"),
- "reagent_name": d.get("material_name"),
- "spec": d.get("spec"),
- "standard_usage": d.get("standard_usage"),
- "actual_usage": d.get("actual_usage"),
- "unit": d.get("unit"),
- "requisition_usage_id": d.get("requisition_usage_id"),
- "is_key_material": d.get("is_key_material", "0"),
- "remark": d.get("remark"),
- })
- production_task_svc.replace_step_reagents(db, step_id, tenant_no, user.get("user_id"), items_data)
- return success_response(None, message="更新成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/steps/{step_id}/consumables")
- async def replace_task_step_consumables(
- task_id: str,
- step_id: str,
- body: List[TaskStepMaterialItem],
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = []
- for it in body:
- d = it.model_dump(by_alias=False)
- items_data.append({
- "consumable_code": d.get("material_code"),
- "consumable_name": d.get("material_name"),
- "spec_model": d.get("spec_model"),
- "standard_usage": d.get("standard_usage"),
- "actual_usage": d.get("actual_usage"),
- "unit": d.get("unit"),
- "requisition_usage_id": d.get("requisition_usage_id"),
- "is_required": d.get("is_required", "1"),
- "remark": d.get("remark"),
- })
- production_task_svc.replace_step_consumables(db, step_id, tenant_no, user.get("user_id"), items_data)
- return success_response(None, message="更新成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/steps/{step_id}/configured-reagents")
- async def replace_task_step_configured_reagents(
- task_id: str,
- step_id: str,
- body: List[TaskStepMaterialItem],
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = []
- for it in body:
- d = it.model_dump(by_alias=False)
- items_data.append({
- "configured_reagent_code": d.get("material_code"),
- "configured_reagent_name": d.get("material_name"),
- "spec": d.get("spec"),
- "standard_usage": d.get("standard_usage"),
- "actual_usage": d.get("actual_usage"),
- "unit": d.get("unit"),
- "requisition_usage_id": d.get("requisition_usage_id"),
- "remark": d.get("remark"),
- })
- production_task_svc.replace_step_configured_reagents(
- db, step_id, tenant_no, user.get("user_id"), items_data
- )
- return success_response(None, message="更新成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/steps/{step_id}/data-collection")
- async def replace_task_step_data_collections(
- task_id: str,
- step_id: str,
- body: List[TaskStepDataCollectionItem],
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body]
- production_task_svc.replace_step_data_collections(db, step_id, tenant_no, user.get("user_id"), items_data)
- return success_response(None, message="更新成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/production-tasks/{task_id}/steps/{step_id}/outputs")
- async def replace_task_step_outputs(
- task_id: str,
- step_id: str,
- body: List[TaskStepOutputItem],
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body]
- production_task_svc.replace_step_outputs(db, step_id, tenant_no, user.get("user_id"), items_data)
- return success_response(None, message="更新成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/production-tasks/{task_id}/inputs")
- async def set_production_task_inputs(
- task_id: str,
- body: List[TaskInputItem],
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body]
- production_task_svc.set_task_inputs(db, task_id, tenant_no, user.get("user_id"), items_data)
- return success_response(None, message="设置成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/production-tasks/{task_id}/outputs")
- async def set_production_task_outputs(
- task_id: str,
- body: List[TaskOutputItem],
- operateScope: Optional[str] = Query(None, alias="operateScope"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items_data = [it.model_dump(by_alias=False) for it in body]
- production_task_svc.set_task_outputs(db, task_id, tenant_no, user.get("user_id"), items_data)
- return success_response(None, message="设置成功")
- except PermissionError as e:
- return error_response(403, str(e))
- except ValueError as e:
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 11. 细胞产品库
- # ---------------------------------------------------------------------------
- @router.get("/cell-products/next-code")
- async def get_next_cell_product_code(
- count: int = Query(1, ge=1, le=50),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- if count <= 1:
- code = cell_product_svc.get_next_code(db, tenant_no)
- return success_response({"code": code})
- codes = cell_product_svc.reserve_next_codes(db, tenant_no, count)
- return success_response({"codes": codes, "code": codes[0] if codes else ""})
- @router.get("/cell-products/max-passage-seq")
- async def get_cell_product_max_passage_seq(
- productCode: str = Query(..., alias="productCode"),
- passage: Optional[str] = None,
- passages: Optional[str] = None,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """查询 `{productCode}-{代次}-XX` 在细胞产品库中的最大两位序号。"""
- tenant_no = _get_tenant_no(db, user)
- base = (productCode or "").strip()
- if not base:
- return error_response(400, "产品编码不能为空")
- passage_list: List[str] = []
- if passages:
- passage_list.extend(p.strip() for p in passages.split(",") if p and p.strip())
- if passage and passage.strip():
- passage_list.append(passage.strip())
- if not passage_list:
- return error_response(400, "请指定代次")
- max_seq_map = cell_product_svc.get_max_passage_product_seq_map(
- db, tenant_no, base, passage_list
- )
- next_seq_map = {p: max_seq_map.get(p, 0) + 1 for p in max_seq_map}
- if len(max_seq_map) == 1:
- only_passage = next(iter(max_seq_map))
- only_max = max_seq_map[only_passage]
- only_next = next_seq_map[only_passage]
- return success_response(
- {
- "passage": only_passage,
- "maxSeq": only_max,
- "nextSeq": only_next,
- "maxSeqByPassage": max_seq_map,
- "nextSeqByPassage": next_seq_map,
- }
- )
- return success_response(
- {"maxSeqByPassage": max_seq_map, "nextSeqByPassage": next_seq_map}
- )
- @router.get("/cell-products/max-suffix-seq")
- async def get_cell_product_max_suffix_seq(
- productCode: str = Query(..., alias="productCode"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """查询 `{productCode}-XX` 在细胞产品库中的最大两位序号。"""
- tenant_no = _get_tenant_no(db, user)
- base = (productCode or "").strip()
- if not base:
- return error_response(400, "产品编码不能为空")
- max_seq = cell_product_svc.get_max_product_suffix_seq(db, tenant_no, base)
- return success_response({"maxSeq": max_seq, "nextSeq": max_seq + 1})
- @router.get("/cell-products/check-code")
- async def check_cell_product_code(
- productCode: str = Query(..., alias="productCode"),
- excludeId: Optional[str] = Query(None, alias="excludeId"),
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """检查产品编码是否已被占用(编辑时可排除当前记录)。"""
- tenant_no = _get_tenant_no(db, user)
- code = (productCode or "").strip()
- if not code:
- return error_response(400, "产品编码不能为空")
- exists = cell_product_svc.product_code_exists(
- db, tenant_no, code, exclude_id=excludeId
- )
- return success_response({"exists": exists, "productCode": code})
- @router.get("/cell-products")
- async def list_cell_products(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
- keyword: Optional[str] = None,
- productCode: Optional[str] = None,
- productName: Optional[str] = None,
- productTypeId: Optional[str] = None,
- productStatus: Optional[str] = None,
- passage: Optional[str] = None,
- sourceType: Optional[str] = None,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- result = cell_product_svc.list_cell_products(
- db, tenant_no=tenant_no, page=page, page_size=pageSize,
- keyword=keyword,
- product_code=productCode, product_name=productName,
- product_type_id=productTypeId, product_status=productStatus,
- passage=passage, source_type=sourceType,
- user_info=user,
- )
- items = result.get("items", [])
- total = result.get("total", 0)
- tp = cell_product_svc.total_pages(total, pageSize)
- return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- @router.get("/cell-products/{product_id}")
- async def get_cell_product_detail(
- product_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- obj = cell_product_svc.get_cell_product(db, product_id, tenant_no)
- if not obj:
- return error_response(404, "细胞产品不存在")
- return success_response(cell_product_svc.product_to_response(db, tenant_no, obj))
- @router.get("/cell-products/{product_id}/chain")
- async def get_cell_product_chain(
- product_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- chain = cell_product_svc.get_product_chain(db, product_id, tenant_no)
- if not chain:
- raise HTTPException(status_code=404, detail="细胞产品不存在")
- return success_response(chain)
- @router.get("/cell-products/{product_id}/lineage-qc")
- async def get_cell_product_lineage_qc(
- product_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- data = cell_product_svc.get_product_lineage_qc(db, product_id, tenant_no)
- if not data:
- raise HTTPException(status_code=404, detail="细胞产品不存在")
- return success_response(data)
- @router.post("/cell-products")
- async def create_cell_product(
- body: CellProductCreate,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- items = body.items or []
- qty = int(body.quantity) if body.quantity is not None else 1
- qty = max(1, min(qty, 50))
- if items:
- created = cell_product_svc.create_cell_products_batch(
- db,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id="",
- data=body,
- items=items,
- require_storage_slot=False,
- )
- elif qty > 1:
- created = cell_product_svc.create_cell_products_by_quantity(
- db,
- tenant_no=tenant_no,
- user_id=user.get("user_id"),
- create_dept_id="",
- data=body,
- count=qty,
- )
- else:
- obj = cell_product_svc.create_cell_product(
- db, tenant_no=tenant_no, user_id=user.get("user_id"),
- create_dept_id="", data=body,
- )
- return success_response(
- cell_product_svc.product_to_response(db, tenant_no, obj), message="创建成功"
- )
- resp_list = [
- cell_product_svc.product_to_response(db, tenant_no, obj) for obj in created
- ]
- n = len(created)
- return success_response(
- {"list": resp_list, "count": n},
- message=f"成功创建 {n} 份细胞产品",
- )
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/cell-products/{product_id}")
- async def update_cell_product(
- product_id: str,
- body: CellProductUpdate,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- obj = cell_product_svc.update_cell_product(
- db, product_id, tenant_no, user.get("user_id"), data=body,
- )
- return success_response(
- cell_product_svc.product_to_response(db, tenant_no, obj), message="更新成功"
- )
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/cell-products/{product_id}")
- async def delete_cell_product(
- product_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- cell_product_svc.delete_cell_product(db, product_id, tenant_no, user.get("user_id"))
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- from app.api.production_material import router as production_material_router
- router.include_router(production_material_router)
|