qc.py 96 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719
  1. """质量检测 — 第三方/实验室主数据、质检工单、质检任务 API"""
  2. from typing import Any, Dict, Iterable, List, Optional
  3. from fastapi import APIRouter, Depends, HTTPException, Query
  4. from pydantic import BaseModel, ConfigDict, Field
  5. from sqlalchemy.orm import Session
  6. from datetime import datetime
  7. from decimal import Decimal
  8. from app.core.database import get_db
  9. from app.core.menu_guard import (
  10. require_admin_or_super_admin,
  11. require_any_menu_code,
  12. require_menu_code,
  13. strip_row_fields,
  14. user_has_menu_perm,
  15. )
  16. MY_QC_MATERIAL_USAGE_VIEW_AMOUNT_PERM = "biz:myQcMaterialUsage:viewAmount"
  17. QC_MATERIAL_VIEW_AMOUNT_PERM = "biz:qcMaterial:viewAmount"
  18. QC_THIRD_PARTY_VIEW_AMOUNT_PERM = "biz:qcThirdPartyCompany:viewAmount"
  19. QC_MATERIAL_AMOUNT_FIELD_NAMES = ("unitPrice", "amount", "remainingAmount", "unit_price")
  20. QC_THIRD_PARTY_FEE_FIELD_NAMES = ("feeAmount", "thirdPartyFeeTotal")
  21. from app.core.response import error_response, success_response
  22. from app.core.utils import parse_datetime
  23. from app.crud import qc_lab_test_item as lab_svc
  24. from app.crud import qc_inspection_item_template as iit_svc
  25. from app.crud import qc_item_param as param_svc
  26. from app.crud import qc_task_result as result_svc
  27. from app.crud import qc_task as task_svc
  28. from app.crud import qc_third_party_company as tpc_svc
  29. from app.crud import qc_third_party_item as tpi_svc
  30. from app.crud import qc_material as mat_svc
  31. from app.crud import qc_material_stock_batch as batch_svc
  32. from app.crud import qc_material_stock_check as stock_check_svc
  33. from app.crud import qc_material_usage as mu_svc
  34. from app.crud import qc_material_amount_auth as amount_auth_svc
  35. from app.crud import qc_material_month_template as month_tpl_svc
  36. from app.crud.qc_material_stock_batch import _parse_date
  37. from app.core.qc_material_amount_auth_constants import QC_MATERIAL_AMOUNT_AUTH_LIST_PERM
  38. from app.crud import inbound_sample_special as inbound_sample_special_svc
  39. from app.crud import qc_work_order as wo_svc
  40. from app.crud.collection_protocol import total_pages
  41. from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload
  42. from app.schemas.qc_work_order import QcWorkOrderCreateBody, QcWorkOrderUpdateBody
  43. from app.schemas.qc_task import QcTaskCreateBody
  44. router = APIRouter(prefix="/web/admin", tags=["质量检测"])
  45. # 协议/捐赠查看等场景只读打开质检任务详情(参数、附件)
  46. QC_TASK_READONLY_MENU_CODES = (
  47. "biz:qcTask:list",
  48. "biz:myQcTask:list",
  49. "biz:cellStorageProtocol:list",
  50. "biz:cellStorageProtocolAudit:list",
  51. "biz:researchDonation:list",
  52. "biz:researchDonationAudit:list",
  53. "biz:storageReleaseReview:list",
  54. "biz:qcWorkOrder:list",
  55. )
  56. # ---------- 第三方检测公司 ----------
  57. class ThirdPartyCompanyBody(BaseModel):
  58. model_config = ConfigDict(populate_by_name=True)
  59. company_code: str = Field(..., alias="companyCode")
  60. company_name: str = Field(..., alias="companyName")
  61. contact_name: Optional[str] = Field(None, alias="contactName")
  62. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  63. company_logo: Optional[str] = Field(None, alias="companyLogo")
  64. address: Optional[str] = None
  65. invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
  66. tax_id: Optional[str] = Field(None, alias="taxId")
  67. bank_name: Optional[str] = Field(None, alias="bankName")
  68. bank_account: Optional[str] = Field(None, alias="bankAccount")
  69. status: str = "1"
  70. order_num: Optional[int] = Field(None, alias="orderNum")
  71. remark: Optional[str] = None
  72. class ThirdPartyCompanyUpdateBody(BaseModel):
  73. model_config = ConfigDict(populate_by_name=True)
  74. company_code: Optional[str] = Field(None, alias="companyCode")
  75. company_name: Optional[str] = Field(None, alias="companyName")
  76. contact_name: Optional[str] = Field(None, alias="contactName")
  77. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  78. company_logo: Optional[str] = Field(None, alias="companyLogo")
  79. address: Optional[str] = None
  80. invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
  81. tax_id: Optional[str] = Field(None, alias="taxId")
  82. bank_name: Optional[str] = Field(None, alias="bankName")
  83. bank_account: Optional[str] = Field(None, alias="bankAccount")
  84. status: Optional[str] = None
  85. order_num: Optional[int] = Field(None, alias="orderNum")
  86. remark: Optional[str] = None
  87. @router.get("/qc-third-party-companies")
  88. async def list_qc_third_party_companies(
  89. page: int = Query(1, ge=1),
  90. pageSize: int = Query(10, ge=1, le=100),
  91. keyword: Optional[str] = None,
  92. status: Optional[str] = None,
  93. db: Session = Depends(get_db),
  94. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  95. ):
  96. try:
  97. tenant_no = _get_tenant_no(db, user)
  98. items, total = tpc_svc.list_companies(
  99. db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
  100. user_info=user
  101. )
  102. return success_response(_page_payload(items, total, page, pageSize))
  103. except PermissionError as e:
  104. return error_response(403, str(e))
  105. except Exception as e:
  106. return error_response(400, str(e))
  107. @router.get("/qc-third-party-companies/options")
  108. async def list_qc_third_party_company_options(
  109. db: Session = Depends(get_db),
  110. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  111. ):
  112. try:
  113. tenant_no = _get_tenant_no(db, user)
  114. return success_response(tpc_svc.list_company_options(db, tenant_no))
  115. except PermissionError as e:
  116. return error_response(403, str(e))
  117. except Exception as e:
  118. return error_response(400, str(e))
  119. @router.post("/qc-third-party-companies")
  120. async def create_qc_third_party_company(
  121. body: ThirdPartyCompanyBody,
  122. db: Session = Depends(get_db),
  123. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  124. ):
  125. try:
  126. tenant_no = _get_tenant_no(db, user)
  127. obj = tpc_svc.create_company(
  128. db,
  129. company_code=body.company_code,
  130. company_name=body.company_name,
  131. contact_name=body.contact_name,
  132. contact_phone=body.contact_phone,
  133. company_logo=body.company_logo,
  134. address=body.address,
  135. invoice_title=body.invoice_title,
  136. tax_id=body.tax_id,
  137. bank_name=body.bank_name,
  138. bank_account=body.bank_account,
  139. status=body.status,
  140. tenant_no=tenant_no,
  141. order_num=body.order_num,
  142. remark=body.remark,
  143. user_id=_get_current_user_id(user),
  144. )
  145. return success_response(tpc_svc._row_to_dict(obj), message="创建成功")
  146. except PermissionError as e:
  147. return error_response(403, str(e))
  148. except ValueError as e:
  149. return error_response(400, str(e))
  150. except Exception as e:
  151. return error_response(400, str(e))
  152. @router.put("/qc-third-party-companies/{company_id}")
  153. async def update_qc_third_party_company(
  154. company_id: str,
  155. body: ThirdPartyCompanyUpdateBody,
  156. db: Session = Depends(get_db),
  157. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  158. ):
  159. try:
  160. tenant_no = _get_tenant_no(db, user)
  161. obj = tpc_svc.update_company(
  162. db,
  163. company_id,
  164. company_code=body.company_code,
  165. company_name=body.company_name,
  166. contact_name=body.contact_name,
  167. contact_phone=body.contact_phone,
  168. company_logo=body.company_logo,
  169. address=body.address,
  170. invoice_title=body.invoice_title,
  171. tax_id=body.tax_id,
  172. bank_name=body.bank_name,
  173. bank_account=body.bank_account,
  174. status=body.status,
  175. order_num=body.order_num,
  176. remark=body.remark,
  177. user_id=_get_current_user_id(user),
  178. tenant_no=tenant_no,
  179. )
  180. return success_response(tpc_svc._row_to_dict(obj), message="更新成功")
  181. except PermissionError as e:
  182. return error_response(403, str(e))
  183. except ValueError as e:
  184. return error_response(400, str(e))
  185. except Exception as e:
  186. return error_response(400, str(e))
  187. @router.delete("/qc-third-party-companies/{company_id}")
  188. async def delete_qc_third_party_company(
  189. company_id: str,
  190. db: Session = Depends(get_db),
  191. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  192. ):
  193. try:
  194. tenant_no = _get_tenant_no(db, user)
  195. tpc_svc.soft_delete_company(db, company_id, tenant_no=tenant_no)
  196. return success_response(None, message="删除成功")
  197. except PermissionError as e:
  198. return error_response(403, str(e))
  199. except ValueError as e:
  200. return error_response(400, str(e))
  201. except Exception as e:
  202. return error_response(400, str(e))
  203. # ---------- 第三方检测项目 ----------
  204. class ThirdPartyItemBody(BaseModel):
  205. model_config = ConfigDict(populate_by_name=True)
  206. company_id: str = Field(..., alias="companyId")
  207. item_code: str = Field(..., alias="itemCode")
  208. item_name: str = Field(..., alias="itemName")
  209. fee_amount: float = Field(..., alias="feeAmount")
  210. status: str = "1"
  211. order_num: Optional[int] = Field(None, alias="orderNum")
  212. remark: Optional[str] = None
  213. class ThirdPartyItemUpdateBody(BaseModel):
  214. model_config = ConfigDict(populate_by_name=True)
  215. company_id: Optional[str] = Field(None, alias="companyId")
  216. item_code: Optional[str] = Field(None, alias="itemCode")
  217. item_name: Optional[str] = Field(None, alias="itemName")
  218. fee_amount: Optional[float] = Field(None, alias="feeAmount")
  219. status: Optional[str] = None
  220. order_num: Optional[int] = Field(None, alias="orderNum")
  221. remark: Optional[str] = None
  222. @router.get("/qc-third-party-items")
  223. async def list_qc_third_party_items(
  224. page: int = Query(1, ge=1),
  225. pageSize: int = Query(10, ge=1, le=100),
  226. keyword: Optional[str] = None,
  227. status: Optional[str] = None,
  228. companyId: Optional[str] = None,
  229. db: Session = Depends(get_db),
  230. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  231. ):
  232. try:
  233. tenant_no = _get_tenant_no(db, user)
  234. items, total = tpi_svc.list_items(
  235. db,
  236. page=page,
  237. page_size=pageSize,
  238. keyword=keyword,
  239. status=status,
  240. company_id=companyId,
  241. tenant_no=tenant_no,
  242. user_info=user,
  243. )
  244. return success_response(_page_payload(items, total, page, pageSize))
  245. except PermissionError as e:
  246. return error_response(403, str(e))
  247. except Exception as e:
  248. return error_response(400, str(e))
  249. @router.get("/qc-third-party-items/active")
  250. async def list_qc_third_party_items_active(
  251. companyId: Optional[str] = None,
  252. db: Session = Depends(get_db),
  253. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  254. ):
  255. try:
  256. tenant_no = _get_tenant_no(db, user)
  257. return success_response(tpi_svc.list_active_items(db, tenant_no, company_id=companyId, user_info=user))
  258. except PermissionError as e:
  259. return error_response(403, str(e))
  260. except Exception as e:
  261. return error_response(400, str(e))
  262. @router.post("/qc-third-party-items")
  263. async def create_qc_third_party_item(
  264. body: ThirdPartyItemBody,
  265. db: Session = Depends(get_db),
  266. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  267. ):
  268. try:
  269. tenant_no = _get_tenant_no(db, user)
  270. obj = tpi_svc.create_item(
  271. db,
  272. company_id=body.company_id,
  273. item_code=body.item_code,
  274. item_name=body.item_name,
  275. fee_amount=body.fee_amount,
  276. status=body.status,
  277. tenant_no=tenant_no,
  278. order_num=body.order_num,
  279. remark=body.remark,
  280. user_id=_get_current_user_id(user),
  281. )
  282. return success_response(tpi_svc._row_to_dict(obj, ""), message="创建成功")
  283. except PermissionError as e:
  284. return error_response(403, str(e))
  285. except ValueError as e:
  286. return error_response(400, str(e))
  287. except Exception as e:
  288. return error_response(400, str(e))
  289. @router.put("/qc-third-party-items/{item_id}")
  290. async def update_qc_third_party_item(
  291. item_id: str,
  292. body: ThirdPartyItemUpdateBody,
  293. db: Session = Depends(get_db),
  294. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  295. ):
  296. try:
  297. tenant_no = _get_tenant_no(db, user)
  298. obj = tpi_svc.update_item(
  299. db,
  300. item_id,
  301. company_id=body.company_id,
  302. item_code=body.item_code,
  303. item_name=body.item_name,
  304. fee_amount=body.fee_amount,
  305. status=body.status,
  306. order_num=body.order_num,
  307. remark=body.remark,
  308. user_id=_get_current_user_id(user),
  309. tenant_no=tenant_no,
  310. )
  311. return success_response(tpi_svc._row_to_dict(obj, ""), message="更新成功")
  312. except PermissionError as e:
  313. return error_response(403, str(e))
  314. except ValueError as e:
  315. return error_response(400, str(e))
  316. except Exception as e:
  317. return error_response(400, str(e))
  318. @router.delete("/qc-third-party-items/{item_id}")
  319. async def delete_qc_third_party_item(
  320. item_id: str,
  321. db: Session = Depends(get_db),
  322. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  323. ):
  324. try:
  325. tenant_no = _get_tenant_no(db, user)
  326. tpi_svc.soft_delete_item(db, item_id, tenant_no=tenant_no)
  327. return success_response(None, message="删除成功")
  328. except PermissionError as e:
  329. return error_response(403, str(e))
  330. except ValueError as e:
  331. return error_response(400, str(e))
  332. except Exception as e:
  333. return error_response(400, str(e))
  334. # ---------- 实验室自检项目 ----------
  335. class LabTestItemBody(BaseModel):
  336. model_config = ConfigDict(populate_by_name=True)
  337. item_code: str = Field(..., alias="itemCode")
  338. item_name: str = Field(..., alias="itemName")
  339. description: Optional[str] = None
  340. status: str = "1"
  341. order_num: Optional[int] = Field(None, alias="orderNum")
  342. remark: Optional[str] = None
  343. class LabTestItemUpdateBody(BaseModel):
  344. model_config = ConfigDict(populate_by_name=True)
  345. item_code: Optional[str] = Field(None, alias="itemCode")
  346. item_name: Optional[str] = Field(None, alias="itemName")
  347. description: Optional[str] = None
  348. status: Optional[str] = None
  349. order_num: Optional[int] = Field(None, alias="orderNum")
  350. remark: Optional[str] = None
  351. # ---------- 检测项目模版(项目组合) ----------
  352. class InspectionItemTemplateBody(BaseModel):
  353. model_config = ConfigDict(populate_by_name=True)
  354. template_name: str = Field(..., alias="templateName")
  355. lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
  356. third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
  357. status: Optional[str] = "1"
  358. order_num: Optional[int] = Field(None, alias="orderNum")
  359. remark: Optional[str] = None
  360. class InspectionItemTemplateUpdateBody(BaseModel):
  361. model_config = ConfigDict(populate_by_name=True)
  362. template_name: Optional[str] = Field(None, alias="templateName")
  363. lab_item_ids: Optional[List[str]] = Field(None, alias="labItemIds")
  364. third_party_item_ids: Optional[List[str]] = Field(None, alias="thirdPartyItemIds")
  365. status: Optional[str] = None
  366. order_num: Optional[int] = Field(None, alias="orderNum")
  367. remark: Optional[str] = None
  368. # ---------- 检测项目参数模板 ----------
  369. class ItemParamBody(BaseModel):
  370. model_config = ConfigDict(populate_by_name=True)
  371. params: List[Dict[str, Any]] = Field(default_factory=list, alias="params")
  372. class TaskResultBody(BaseModel):
  373. model_config = ConfigDict(populate_by_name=True)
  374. results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
  375. class TaskCompleteWithResultBody(BaseModel):
  376. model_config = ConfigDict(populate_by_name=True)
  377. conclusion: Optional[str] = Field(None, alias="conclusion")
  378. results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
  379. remark: Optional[str] = Field(None, alias="remark")
  380. third_party_attachments: Optional[str] = Field(None, alias="thirdPartyAttachments")
  381. material_usages: Optional[List[Dict[str, Any]]] = Field(None, alias="materialUsages")
  382. task_status: Optional[str] = Field(None, alias="taskStatus")
  383. @router.get("/qc-lab-test-items")
  384. async def list_qc_lab_test_items(
  385. page: int = Query(1, ge=1),
  386. pageSize: int = Query(10, ge=1, le=100),
  387. keyword: Optional[str] = None,
  388. status: Optional[str] = None,
  389. db: Session = Depends(get_db),
  390. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  391. ):
  392. try:
  393. tenant_no = _get_tenant_no(db, user)
  394. items, total = lab_svc.list_lab_items(
  395. db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
  396. user_info=user
  397. )
  398. return success_response(_page_payload(items, total, page, pageSize))
  399. except PermissionError as e:
  400. return error_response(403, str(e))
  401. except Exception as e:
  402. return error_response(400, str(e))
  403. @router.get("/qc-lab-test-items/active")
  404. async def list_qc_lab_test_items_active(
  405. db: Session = Depends(get_db),
  406. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  407. ):
  408. try:
  409. tenant_no = _get_tenant_no(db, user)
  410. return success_response(lab_svc.list_active_lab_items(db, tenant_no, user_info=user))
  411. except PermissionError as e:
  412. return error_response(403, str(e))
  413. except Exception as e:
  414. return error_response(400, str(e))
  415. @router.post("/qc-lab-test-items")
  416. async def create_qc_lab_test_item(
  417. body: LabTestItemBody,
  418. db: Session = Depends(get_db),
  419. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  420. ):
  421. try:
  422. tenant_no = _get_tenant_no(db, user)
  423. obj = lab_svc.create_lab_item(
  424. db,
  425. item_code=body.item_code,
  426. item_name=body.item_name,
  427. description=body.description,
  428. status=body.status,
  429. tenant_no=tenant_no,
  430. order_num=body.order_num,
  431. remark=body.remark,
  432. user_id=_get_current_user_id(user),
  433. )
  434. return success_response(lab_svc._row_to_dict(obj), message="创建成功")
  435. except PermissionError as e:
  436. return error_response(403, str(e))
  437. except ValueError as e:
  438. return error_response(400, str(e))
  439. except Exception as e:
  440. return error_response(400, str(e))
  441. @router.put("/qc-lab-test-items/{item_id}")
  442. async def update_qc_lab_test_item(
  443. item_id: str,
  444. body: LabTestItemUpdateBody,
  445. db: Session = Depends(get_db),
  446. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  447. ):
  448. try:
  449. tenant_no = _get_tenant_no(db, user)
  450. obj = lab_svc.update_lab_item(
  451. db,
  452. item_id,
  453. item_code=body.item_code,
  454. item_name=body.item_name,
  455. description=body.description,
  456. status=body.status,
  457. order_num=body.order_num,
  458. remark=body.remark,
  459. user_id=_get_current_user_id(user),
  460. tenant_no=tenant_no,
  461. )
  462. return success_response(lab_svc._row_to_dict(obj), message="更新成功")
  463. except PermissionError as e:
  464. return error_response(403, str(e))
  465. except ValueError as e:
  466. return error_response(400, str(e))
  467. except Exception as e:
  468. return error_response(400, str(e))
  469. @router.delete("/qc-lab-test-items/{item_id}")
  470. async def delete_qc_lab_test_item(
  471. item_id: str,
  472. db: Session = Depends(get_db),
  473. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  474. ):
  475. try:
  476. tenant_no = _get_tenant_no(db, user)
  477. lab_svc.soft_delete_lab_item(db, item_id, tenant_no=tenant_no)
  478. return success_response(None, message="删除成功")
  479. except PermissionError as e:
  480. return error_response(403, str(e))
  481. except ValueError as e:
  482. return error_response(400, str(e))
  483. except Exception as e:
  484. return error_response(400, str(e))
  485. @router.get("/qc-inspection-item-templates")
  486. async def list_qc_inspection_item_templates(
  487. page: int = Query(1, ge=1),
  488. pageSize: int = Query(10, ge=1, le=100),
  489. keyword: Optional[str] = None,
  490. status: Optional[str] = None,
  491. db: Session = Depends(get_db),
  492. user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:list")),
  493. ):
  494. try:
  495. tenant_no = _get_tenant_no(db, user)
  496. items, total = iit_svc.list_templates(
  497. db,
  498. page=page,
  499. page_size=pageSize,
  500. keyword=keyword,
  501. status=status,
  502. tenant_no=tenant_no,
  503. user_info=user,
  504. )
  505. return success_response(_page_payload(items, total, page, pageSize))
  506. except PermissionError as e:
  507. return error_response(403, str(e))
  508. except Exception as e:
  509. return error_response(400, str(e))
  510. @router.get("/qc-inspection-item-templates/active")
  511. async def list_qc_inspection_item_templates_active(
  512. db: Session = Depends(get_db),
  513. user: dict = Depends(
  514. require_any_menu_code(
  515. "biz:qcInspectionItemTemplate:list", "biz:qcWorkOrder:list"
  516. )
  517. ),
  518. ):
  519. try:
  520. tenant_no = _get_tenant_no(db, user)
  521. return success_response(
  522. iit_svc.list_active_templates(db, tenant_no, user_info=user)
  523. )
  524. except PermissionError as e:
  525. return error_response(403, str(e))
  526. except Exception as e:
  527. return error_response(400, str(e))
  528. @router.get("/qc-inspection-item-templates/{template_id}")
  529. async def get_qc_inspection_item_template(
  530. template_id: str,
  531. db: Session = Depends(get_db),
  532. user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:list")),
  533. ):
  534. try:
  535. tenant_no = _get_tenant_no(db, user)
  536. obj = iit_svc.get_template(db, template_id, tenant_no=tenant_no)
  537. if not obj:
  538. return error_response(404, "模版不存在")
  539. return success_response(iit_svc._row_to_dict(obj))
  540. except PermissionError as e:
  541. return error_response(403, str(e))
  542. except Exception as e:
  543. return error_response(400, str(e))
  544. @router.post("/qc-inspection-item-templates")
  545. async def create_qc_inspection_item_template(
  546. body: InspectionItemTemplateBody,
  547. db: Session = Depends(get_db),
  548. user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:add")),
  549. ):
  550. try:
  551. tenant_no = _get_tenant_no(db, user)
  552. obj = iit_svc.create_template(
  553. db,
  554. template_name=body.template_name,
  555. lab_item_ids=body.lab_item_ids,
  556. third_party_item_ids=body.third_party_item_ids,
  557. status=body.status or "1",
  558. tenant_no=tenant_no,
  559. order_num=body.order_num,
  560. remark=body.remark,
  561. user_id=_get_current_user_id(user),
  562. create_dept_id=user.get("dept_id", "") or "",
  563. )
  564. return success_response(iit_svc._row_to_dict(obj), message="创建成功")
  565. except PermissionError as e:
  566. return error_response(403, str(e))
  567. except ValueError as e:
  568. return error_response(400, str(e))
  569. except Exception as e:
  570. return error_response(400, str(e))
  571. @router.put("/qc-inspection-item-templates/{template_id}")
  572. async def update_qc_inspection_item_template(
  573. template_id: str,
  574. body: InspectionItemTemplateUpdateBody,
  575. db: Session = Depends(get_db),
  576. user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:edit")),
  577. ):
  578. try:
  579. tenant_no = _get_tenant_no(db, user)
  580. obj = iit_svc.update_template(
  581. db,
  582. template_id,
  583. template_name=body.template_name,
  584. lab_item_ids=body.lab_item_ids,
  585. third_party_item_ids=body.third_party_item_ids,
  586. status=body.status,
  587. order_num=body.order_num,
  588. remark=body.remark,
  589. user_id=_get_current_user_id(user),
  590. tenant_no=tenant_no,
  591. )
  592. return success_response(iit_svc._row_to_dict(obj), message="更新成功")
  593. except PermissionError as e:
  594. return error_response(403, str(e))
  595. except ValueError as e:
  596. return error_response(400, str(e))
  597. except Exception as e:
  598. return error_response(400, str(e))
  599. @router.delete("/qc-inspection-item-templates/{template_id}")
  600. async def delete_qc_inspection_item_template(
  601. template_id: str,
  602. db: Session = Depends(get_db),
  603. user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:remove")),
  604. ):
  605. try:
  606. tenant_no = _get_tenant_no(db, user)
  607. iit_svc.soft_delete_template(db, template_id, tenant_no=tenant_no)
  608. return success_response(None, message="删除成功")
  609. except PermissionError as e:
  610. return error_response(403, str(e))
  611. except ValueError as e:
  612. return error_response(400, str(e))
  613. except Exception as e:
  614. return error_response(400, str(e))
  615. # ---------- 检测项目参数模板 ----------
  616. @router.get("/qc-item-params")
  617. async def list_item_params(
  618. itemSource: str = Query(...),
  619. itemId: str = Query(...),
  620. db: Session = Depends(get_db),
  621. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  622. ):
  623. try:
  624. tenant_no = _get_tenant_no(db, user)
  625. return success_response(param_svc.list_params_by_item(db, itemSource, itemId, tenant_no, user_info=user))
  626. except PermissionError as e:
  627. return error_response(403, str(e))
  628. except Exception as e:
  629. return error_response(400, str(e))
  630. @router.put("/qc-item-params")
  631. async def save_item_params(
  632. body: ItemParamBody,
  633. itemSource: str = Query(...),
  634. itemId: str = Query(...),
  635. db: Session = Depends(get_db),
  636. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  637. ):
  638. try:
  639. tenant_no = _get_tenant_no(db, user)
  640. return success_response(
  641. param_svc.save_params_for_item(db, itemSource, itemId, body.params, tenant_no, _get_current_user_id(user)),
  642. message="保存成功",
  643. )
  644. except PermissionError as e:
  645. return error_response(403, str(e))
  646. except Exception as e:
  647. return error_response(400, str(e))
  648. # ---------- 质检工单 ----------
  649. class WorkOrderConfigureBatchItem(BaseModel):
  650. model_config = ConfigDict(populate_by_name=True)
  651. id: Optional[str] = None
  652. batch_name: Optional[str] = Field(None, alias="batchName")
  653. assignee_user_ids: List[str] = Field(default_factory=list, alias="assigneeUserIds")
  654. has_lab: bool = Field(False, alias="hasLab")
  655. has_third_party: bool = Field(False, alias="hasThirdParty")
  656. lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
  657. third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
  658. class WorkOrderConfigureBody(BaseModel):
  659. model_config = ConfigDict(populate_by_name=True)
  660. batches: Optional[List[WorkOrderConfigureBatchItem]] = Field(None, alias="batches")
  661. has_lab: bool = Field(False, alias="hasLab")
  662. has_third_party: bool = Field(False, alias="hasThirdParty")
  663. lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
  664. third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
  665. lab_item_assignees: Dict[str, List[str]] = Field(default_factory=dict, alias="labItemAssignees")
  666. third_party_item_assignees: Dict[str, List[str]] = Field(
  667. default_factory=dict, alias="thirdPartyItemAssignees"
  668. )
  669. @router.post("/qc-work-orders")
  670. async def create_qc_work_order(
  671. body: QcWorkOrderCreateBody,
  672. db: Session = Depends(get_db),
  673. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  674. ):
  675. try:
  676. tenant_no = _get_tenant_no(db, user)
  677. wo_type = (body.work_order_type or "inbound").strip()
  678. if wo_type == "temporary":
  679. wo = wo_svc.create_temp_qc_work_order(
  680. db,
  681. tenant_no=tenant_no,
  682. user_id=user.get("user_id"),
  683. create_dept_id=user.get("dept_id", ""),
  684. cell_product_id=body.cell_product_id or "",
  685. external_target_code=body.external_target_code or "",
  686. remark=body.remark or "",
  687. )
  688. else:
  689. wo = wo_svc.create_qc_work_order_manual(
  690. db,
  691. tenant_no=tenant_no,
  692. user_id=user.get("user_id"),
  693. create_dept_id=user.get("dept_id", ""),
  694. protocol_no=body.protocol_no or "",
  695. sample_code=body.sample_code or "",
  696. reception_source=body.reception_source or "cell_storage",
  697. qc_biz_type=body.qc_biz_type or "inbound",
  698. remark=body.remark or "",
  699. )
  700. db.commit()
  701. return success_response(wo_svc._wo_to_dict(wo), message="创建成功")
  702. except PermissionError as e:
  703. return error_response(403, str(e))
  704. except Exception as e:
  705. db.rollback()
  706. return error_response(400, str(e))
  707. @router.get("/qc-work-orders")
  708. async def list_qc_work_orders(
  709. page: int = Query(1, ge=1),
  710. pageSize: int = Query(10, ge=1, le=100),
  711. keyword: Optional[str] = None,
  712. workOrderStatus: Optional[str] = Query(None, description="工单状态,多个用英文逗号分隔"),
  713. workOrderType: Optional[str] = None,
  714. bizType: Optional[str] = Query(None, alias="bizType"),
  715. receptionSource: Optional[str] = Query(None, alias="receptionSource"),
  716. createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
  717. createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
  718. infusionTimeStart: Optional[str] = Query(None, alias="infusionTimeStart"),
  719. infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
  720. triggerSource: Optional[str] = Query(None, alias="triggerSource"),
  721. sampleTypeId: Optional[str] = Query(None, alias="sampleTypeId"),
  722. db: Session = Depends(get_db),
  723. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  724. ):
  725. try:
  726. tenant_no = _get_tenant_no(db, user)
  727. items, total = wo_svc.list_work_orders(
  728. db,
  729. page=page,
  730. page_size=pageSize,
  731. keyword=keyword,
  732. work_order_status=workOrderStatus,
  733. work_order_type=workOrderType,
  734. biz_type=bizType,
  735. reception_source=receptionSource,
  736. trigger_source=triggerSource,
  737. sample_type_id=sampleTypeId,
  738. create_time_start=createTimeStart,
  739. create_time_end=createTimeEnd,
  740. infusion_time_start=infusionTimeStart,
  741. infusion_time_end=infusionTimeEnd,
  742. tenant_no=tenant_no,
  743. user_info=user,
  744. )
  745. return success_response(_page_payload(items, total, page, pageSize))
  746. except PermissionError as e:
  747. return error_response(403, str(e))
  748. except Exception as e:
  749. return error_response(400, str(e))
  750. @router.get("/qc-work-orders/tab-unfinished-counts")
  751. async def qc_work_order_tab_unfinished_counts(
  752. db: Session = Depends(get_db),
  753. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  754. ):
  755. try:
  756. tenant_no = _get_tenant_no(db, user)
  757. counts = wo_svc.count_unfinished_work_orders_by_tab(
  758. db, tenant_no=tenant_no, user_info=user
  759. )
  760. return success_response(counts)
  761. except PermissionError as e:
  762. return error_response(403, str(e))
  763. except Exception as e:
  764. return error_response(400, str(e))
  765. @router.get("/qc-work-orders/mine-tab-unfinished-counts")
  766. async def qc_work_order_mine_tab_unfinished_counts(
  767. db: Session = Depends(get_db),
  768. user: dict = Depends(require_menu_code("biz:myQcTask:list")),
  769. ):
  770. try:
  771. tenant_no = _get_tenant_no(db, user)
  772. counts = wo_svc.count_mine_unfinished_work_orders_by_tab(
  773. db,
  774. tenant_no=tenant_no,
  775. assignee_user_id=_get_current_user_id(user),
  776. )
  777. return success_response(counts)
  778. except PermissionError as e:
  779. return error_response(403, str(e))
  780. except Exception as e:
  781. return error_response(400, str(e))
  782. @router.get("/qc-work-orders/by-sample")
  783. async def list_qc_work_orders_by_sample(
  784. sampleCode: str = Query(..., alias="sampleCode"),
  785. receptionSource: str = Query(..., alias="receptionSource"),
  786. db: Session = Depends(get_db),
  787. user: dict = Depends(
  788. require_any_menu_code("biz:qcWorkOrder:list", "biz:myQcTask:list")
  789. ),
  790. ):
  791. try:
  792. tenant_no = _get_tenant_no(db, user)
  793. items = wo_svc.list_work_orders_by_sample(
  794. db,
  795. sample_code=sampleCode,
  796. reception_source=receptionSource,
  797. tenant_no=tenant_no,
  798. user_info=user,
  799. )
  800. return success_response({"list": items})
  801. except PermissionError as e:
  802. return error_response(403, str(e))
  803. except Exception as e:
  804. return error_response(400, str(e))
  805. @router.get("/qc-work-orders/{work_order_id}")
  806. async def get_qc_work_order_detail(
  807. work_order_id: str,
  808. db: Session = Depends(get_db),
  809. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  810. ):
  811. try:
  812. tenant_no = _get_tenant_no(db, user)
  813. data = wo_svc.get_work_order_detail(db, work_order_id, tenant_no)
  814. _maybe_strip_qc_third_party_fee_fields(db, user, data)
  815. return success_response(data)
  816. except PermissionError as e:
  817. return error_response(403, str(e))
  818. except ValueError as e:
  819. return error_response(400, str(e))
  820. except Exception as e:
  821. return error_response(400, str(e))
  822. @router.get("/qc-work-orders/{work_order_id}/outbound-detail")
  823. async def get_outbound_qc_detail(
  824. work_order_id: str,
  825. db: Session = Depends(get_db),
  826. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  827. ):
  828. try:
  829. tenant_no = _get_tenant_no(db, user)
  830. data = wo_svc.get_outbound_qc_detail(db, work_order_id, tenant_no)
  831. _maybe_strip_qc_third_party_fee_fields(db, user, data)
  832. return success_response(data)
  833. except PermissionError as e:
  834. return error_response(403, str(e))
  835. except ValueError as e:
  836. return error_response(400, str(e))
  837. except Exception as e:
  838. return error_response(400, str(e))
  839. @router.put("/qc-work-orders/{work_order_id}")
  840. async def update_qc_work_order(
  841. work_order_id: str,
  842. body: QcWorkOrderUpdateBody,
  843. db: Session = Depends(get_db),
  844. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  845. ):
  846. try:
  847. tenant_no = _get_tenant_no(db, user)
  848. wo = wo_svc.update_qc_work_order(
  849. db,
  850. work_order_id,
  851. tenant_no=tenant_no,
  852. user_id=_get_current_user_id(user),
  853. cell_product_id=body.cell_product_id,
  854. external_target_code=body.external_target_code,
  855. work_order_status=body.work_order_status,
  856. qc_conclusion=body.qc_conclusion,
  857. remark=body.remark,
  858. )
  859. db.commit()
  860. return success_response(wo_svc._wo_to_dict(wo), message="保存成功")
  861. except PermissionError as e:
  862. return error_response(403, str(e))
  863. except ValueError as e:
  864. db.rollback()
  865. return error_response(400, str(e))
  866. except Exception as e:
  867. db.rollback()
  868. return error_response(400, str(e))
  869. @router.delete("/qc-work-orders/{work_order_id}")
  870. async def delete_qc_work_order_api(
  871. work_order_id: str,
  872. user: dict = Depends(require_admin_or_super_admin),
  873. db: Session = Depends(get_db),
  874. ):
  875. try:
  876. tenant_no = _get_tenant_no(db, user)
  877. wo_svc.delete_qc_work_order(db, work_order_id, tenant_no, user.get("user_id"))
  878. return success_response(None, message="删除成功")
  879. except ValueError as e:
  880. return error_response(400, str(e))
  881. class InboundSampleSpecialMarkBody(BaseModel):
  882. model_config = ConfigDict(populate_by_name=True)
  883. reception_source: str = Field(..., alias="receptionSource")
  884. protocol_sample_id: str = Field(..., alias="protocolSampleId")
  885. is_special: bool = Field(..., alias="isSpecial")
  886. special_remark: Optional[str] = Field(None, alias="specialRemark")
  887. @router.put("/inbound-samples/special-mark")
  888. async def update_inbound_sample_special_mark(
  889. body: InboundSampleSpecialMarkBody,
  890. db: Session = Depends(get_db),
  891. user: dict = Depends(
  892. require_any_menu_code("biz:qcWorkOrder:list", "biz:myQcTask:list")
  893. ),
  894. ):
  895. try:
  896. tenant_no = _get_tenant_no(db, user)
  897. source = (body.reception_source or "").strip()
  898. if source not in ("cell_storage", "research_donation"):
  899. return error_response(400, "仅支持细胞存储或科研捐赠样本")
  900. data = inbound_sample_special_svc.update_inbound_sample_special_mark(
  901. db,
  902. tenant_no=tenant_no,
  903. reception_source=source,
  904. protocol_sample_id=body.protocol_sample_id,
  905. is_special=body.is_special,
  906. special_remark=body.special_remark,
  907. user_id=user.get("user_id") or "",
  908. )
  909. db.commit()
  910. return success_response(data, message="保存成功")
  911. except ValueError as e:
  912. db.rollback()
  913. return error_response(400, str(e))
  914. except Exception as e:
  915. db.rollback()
  916. return error_response(400, str(e))
  917. @router.post("/qc-work-orders/{work_order_id}/configure")
  918. async def configure_qc_work_order(
  919. work_order_id: str,
  920. body: WorkOrderConfigureBody,
  921. db: Session = Depends(get_db),
  922. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  923. ):
  924. try:
  925. tenant_no = _get_tenant_no(db, user)
  926. obj = wo_svc.configure_work_order(
  927. db,
  928. work_order_id,
  929. has_lab=body.has_lab,
  930. has_third_party=body.has_third_party,
  931. lab_item_ids=body.lab_item_ids,
  932. third_party_item_ids=body.third_party_item_ids,
  933. lab_item_assignees=body.lab_item_assignees,
  934. third_party_item_assignees=body.third_party_item_assignees,
  935. batches=[b.model_dump(by_alias=True) for b in body.batches] if body.batches is not None else None,
  936. tenant_no=tenant_no,
  937. user_id=_get_current_user_id(user),
  938. create_dept_id=user.get("dept_id", ""),
  939. )
  940. return success_response(wo_svc._wo_to_dict(obj), message="配置成功")
  941. except PermissionError as e:
  942. return error_response(403, str(e))
  943. except ValueError as e:
  944. return error_response(400, str(e))
  945. except Exception as e:
  946. return error_response(400, str(e))
  947. class WorkOrderInspectionBatchesBatchBody(BaseModel):
  948. model_config = ConfigDict(populate_by_name=True)
  949. work_order_ids: List[str] = Field(default_factory=list, alias="workOrderIds")
  950. @router.post("/qc-work-orders/inspection-batches/batch")
  951. async def batch_list_qc_work_order_inspection_batches(
  952. body: WorkOrderInspectionBatchesBatchBody,
  953. db: Session = Depends(get_db),
  954. user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
  955. ):
  956. try:
  957. tenant_no = _get_tenant_no(db, user)
  958. from app.crud import qc_inspection_batch as batch_svc
  959. data = batch_svc.list_inspection_batches_by_work_order_ids(
  960. db, body.work_order_ids, tenant_no
  961. )
  962. return success_response(data)
  963. except PermissionError as e:
  964. return error_response(403, str(e))
  965. except Exception as e:
  966. return error_response(400, str(e))
  967. @router.get("/qc-work-orders/{work_order_id}/inspection-batches")
  968. async def list_qc_work_order_inspection_batches(
  969. work_order_id: str,
  970. db: Session = Depends(get_db),
  971. user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
  972. ):
  973. try:
  974. tenant_no = _get_tenant_no(db, user)
  975. from app.crud import qc_inspection_batch as batch_svc
  976. wo = wo_svc.get_work_order(db, work_order_id, tenant_no=tenant_no)
  977. if not wo:
  978. return error_response(404, "工单不存在")
  979. return success_response(batch_svc.list_inspection_batches(db, work_order_id, tenant_no))
  980. except PermissionError as e:
  981. return error_response(403, str(e))
  982. except Exception as e:
  983. return error_response(400, str(e))
  984. # ---------- 质检任务 ----------
  985. class TaskAttachmentsBody(BaseModel):
  986. model_config = ConfigDict(populate_by_name=True)
  987. third_party_attachments: str = Field("", alias="thirdPartyAttachments")
  988. @router.post("/qc-tasks")
  989. async def create_qc_task(
  990. body: QcTaskCreateBody,
  991. db: Session = Depends(get_db),
  992. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  993. ):
  994. try:
  995. tenant_no = _get_tenant_no(db, user)
  996. task = task_svc.create_qc_task_manual(
  997. db,
  998. tenant_no=tenant_no,
  999. user_id=user.get("user_id"),
  1000. create_dept_id=user.get("dept_id", ""),
  1001. task_type=body.task_type,
  1002. item_code=body.item_code,
  1003. item_name=body.item_name,
  1004. company_id=body.company_id,
  1005. company_name=body.company_name,
  1006. fee_amount=body.fee_amount,
  1007. sample_code=body.sample_code or "",
  1008. work_order_id=body.work_order_id or "",
  1009. remark=body.remark or "",
  1010. )
  1011. db.commit()
  1012. return success_response(task_svc._task_to_dict(task), message="创建成功")
  1013. except PermissionError as e:
  1014. return error_response(403, str(e))
  1015. except Exception as e:
  1016. db.rollback()
  1017. return error_response(400, str(e))
  1018. @router.get("/qc-tasks/mine")
  1019. async def list_my_qc_tasks(
  1020. page: int = Query(1, ge=1),
  1021. pageSize: int = Query(10, ge=1, le=100),
  1022. keyword: Optional[str] = None,
  1023. taskType: Optional[str] = None,
  1024. taskStatus: Optional[str] = None,
  1025. qcBizType: Optional[str] = None,
  1026. receptionSource: Optional[str] = Query(None, alias="receptionSource"),
  1027. bizType: Optional[str] = Query(None, alias="bizType"),
  1028. db: Session = Depends(get_db),
  1029. user: dict = Depends(require_menu_code("biz:myQcTask:list")),
  1030. ):
  1031. try:
  1032. tenant_no = _get_tenant_no(db, user)
  1033. items, total = task_svc.list_mine_tasks(
  1034. db,
  1035. assignee_user_id=_get_current_user_id(user),
  1036. page=page,
  1037. page_size=pageSize,
  1038. keyword=keyword,
  1039. task_type=taskType,
  1040. task_status=taskStatus,
  1041. qc_biz_type=qcBizType,
  1042. reception_source=receptionSource,
  1043. biz_type=bizType,
  1044. tenant_no=tenant_no,
  1045. )
  1046. return success_response(_page_payload(items, total, page, pageSize))
  1047. except PermissionError as e:
  1048. return error_response(403, str(e))
  1049. except Exception as e:
  1050. return error_response(400, str(e))
  1051. @router.get("/qc-tasks")
  1052. async def list_qc_tasks(
  1053. page: int = Query(1, ge=1),
  1054. pageSize: int = Query(10, ge=1, le=100),
  1055. keyword: Optional[str] = None,
  1056. taskType: Optional[str] = None,
  1057. taskStatus: Optional[str] = None,
  1058. workOrderId: Optional[str] = None,
  1059. qcBizType: Optional[str] = None,
  1060. db: Session = Depends(get_db),
  1061. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  1062. ):
  1063. try:
  1064. tenant_no = _get_tenant_no(db, user)
  1065. items, total = task_svc.list_tasks(
  1066. db,
  1067. page=page,
  1068. page_size=pageSize,
  1069. keyword=keyword,
  1070. task_type=taskType,
  1071. task_status=taskStatus,
  1072. work_order_id=workOrderId,
  1073. qc_biz_type=qcBizType,
  1074. tenant_no=tenant_no,
  1075. user_info=user,
  1076. )
  1077. return success_response(_page_payload(items, total, page, pageSize))
  1078. except PermissionError as e:
  1079. return error_response(403, str(e))
  1080. except Exception as e:
  1081. return error_response(400, str(e))
  1082. @router.get("/qc-tasks/{task_id}")
  1083. async def get_qc_task_detail(
  1084. task_id: str,
  1085. db: Session = Depends(get_db),
  1086. user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
  1087. ):
  1088. try:
  1089. tenant_no = _get_tenant_no(db, user)
  1090. data = task_svc.get_task_detail(db, task_id, tenant_no)
  1091. return success_response(data)
  1092. except PermissionError as e:
  1093. return error_response(403, str(e))
  1094. except ValueError as e:
  1095. return error_response(400, str(e))
  1096. except Exception as e:
  1097. return error_response(400, str(e))
  1098. @router.post("/qc-tasks/{task_id}/start")
  1099. async def start_qc_task(
  1100. task_id: str,
  1101. operateScope: Optional[str] = Query(None, alias="operateScope"),
  1102. db: Session = Depends(get_db),
  1103. user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
  1104. ):
  1105. try:
  1106. tenant_no = _get_tenant_no(db, user)
  1107. obj = task_svc.start_task(db, task_id, tenant_no=tenant_no, user_id=_get_current_user_id(user))
  1108. return success_response(task_svc._task_to_dict(obj), message="已开始")
  1109. except PermissionError as e:
  1110. return error_response(403, str(e))
  1111. except ValueError as e:
  1112. return error_response(400, str(e))
  1113. except Exception as e:
  1114. return error_response(400, str(e))
  1115. @router.post("/qc-tasks/{task_id}/complete")
  1116. async def complete_qc_task(
  1117. task_id: str,
  1118. db: Session = Depends(get_db),
  1119. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  1120. ):
  1121. try:
  1122. tenant_no = _get_tenant_no(db, user)
  1123. obj = task_svc.complete_task(
  1124. db, task_id, tenant_no=tenant_no, user_id=_get_current_user_id(user)
  1125. )
  1126. return success_response(task_svc._task_to_dict(obj), message="已完成")
  1127. except PermissionError as e:
  1128. return error_response(403, str(e))
  1129. except ValueError as e:
  1130. return error_response(400, str(e))
  1131. except Exception as e:
  1132. return error_response(400, str(e))
  1133. @router.get("/qc-tasks/{task_id}/results")
  1134. async def get_task_results(
  1135. task_id: str,
  1136. db: Session = Depends(get_db),
  1137. user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
  1138. ):
  1139. try:
  1140. tenant_no = _get_tenant_no(db, user)
  1141. return success_response(result_svc.list_results_by_task(db, task_id, tenant_no, user_info=user))
  1142. except PermissionError as e:
  1143. return error_response(403, str(e))
  1144. except Exception as e:
  1145. return error_response(400, str(e))
  1146. @router.get("/qc-material-requisition-options")
  1147. async def list_qc_material_requisition_options(
  1148. keyword: Optional[str] = None,
  1149. limit: int = Query(50, ge=1, le=100),
  1150. db: Session = Depends(get_db),
  1151. user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
  1152. ):
  1153. try:
  1154. tenant_no = _get_tenant_no(db, user)
  1155. items = mu_svc.list_requisition_options(
  1156. db,
  1157. tenant_no,
  1158. keyword=keyword,
  1159. limit=limit,
  1160. user_info=user,
  1161. )
  1162. return success_response(items)
  1163. except PermissionError as e:
  1164. return error_response(403, str(e))
  1165. except Exception as e:
  1166. return error_response(400, str(e))
  1167. @router.get("/qc-tasks/{task_id}/material-usages")
  1168. async def get_task_material_usages(
  1169. task_id: str,
  1170. db: Session = Depends(get_db),
  1171. user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
  1172. ):
  1173. try:
  1174. tenant_no = _get_tenant_no(db, user)
  1175. t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
  1176. if not t:
  1177. return error_response(404, "任务不存在")
  1178. return success_response(
  1179. mu_svc.list_usages_by_task(db, task_id, tenant_no, user_info=user)
  1180. )
  1181. except PermissionError as e:
  1182. return error_response(403, str(e))
  1183. except Exception as e:
  1184. return error_response(400, str(e))
  1185. @router.post("/qc-tasks/{task_id}/complete-with-result")
  1186. async def complete_task_with_result(
  1187. task_id: str,
  1188. body: TaskCompleteWithResultBody,
  1189. operateScope: Optional[str] = Query(None, alias="operateScope"),
  1190. db: Session = Depends(get_db),
  1191. user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
  1192. ):
  1193. try:
  1194. tenant_no = _get_tenant_no(db, user)
  1195. user_id = _get_current_user_id(user)
  1196. t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
  1197. if not t:
  1198. return error_response(404, "任务不存在")
  1199. was_done = (t.task_status or "").strip() == "ywc"
  1200. if body.results:
  1201. result_svc.save_results_for_task(
  1202. db, task_id, body.results, tenant_no, user_id, commit=False,
  1203. )
  1204. if body.third_party_attachments is not None:
  1205. task_svc.update_task_attachments(
  1206. db, task_id, attachments=body.third_party_attachments,
  1207. tenant_no=tenant_no, user_id=user_id, commit=False,
  1208. )
  1209. obj = task_svc.apply_task_result(
  1210. db,
  1211. task_id,
  1212. tenant_no=tenant_no,
  1213. user_id=user_id,
  1214. conclusion=body.conclusion,
  1215. remark=body.remark,
  1216. material_usages=body.material_usages,
  1217. task_status=body.task_status,
  1218. commit=False,
  1219. )
  1220. db.commit()
  1221. db.refresh(obj)
  1222. if (obj.task_status or "").strip() == "ywc":
  1223. wo_svc.try_complete_work_order(db, obj.work_order_id, tenant_no)
  1224. elif was_done:
  1225. wo_svc.try_reopen_work_order(db, obj.work_order_id, tenant_no)
  1226. return success_response(
  1227. task_svc._task_to_dict(obj, work_order_no="", db=db, tenant_no=tenant_no),
  1228. message="已保存",
  1229. )
  1230. except HTTPException as e:
  1231. db.rollback()
  1232. detail = e.detail
  1233. msg = detail if isinstance(detail, str) else str(detail)
  1234. return error_response(e.status_code, msg)
  1235. except PermissionError as e:
  1236. db.rollback()
  1237. return error_response(403, str(e))
  1238. except ValueError as e:
  1239. db.rollback()
  1240. return error_response(400, str(e))
  1241. except Exception as e:
  1242. db.rollback()
  1243. return error_response(400, str(e))
  1244. @router.put("/qc-tasks/{task_id}/attachments")
  1245. async def update_qc_task_attachments(
  1246. task_id: str,
  1247. body: TaskAttachmentsBody,
  1248. operateScope: Optional[str] = Query(None, alias="operateScope"),
  1249. db: Session = Depends(get_db),
  1250. user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
  1251. ):
  1252. try:
  1253. tenant_no = _get_tenant_no(db, user)
  1254. obj = task_svc.update_task_attachments(
  1255. db,
  1256. task_id,
  1257. attachments=body.third_party_attachments,
  1258. tenant_no=tenant_no,
  1259. user_id=_get_current_user_id(user),
  1260. )
  1261. return success_response(task_svc._task_to_dict(obj), message="已保存")
  1262. except PermissionError as e:
  1263. return error_response(403, str(e))
  1264. except ValueError as e:
  1265. return error_response(400, str(e))
  1266. except Exception as e:
  1267. return error_response(400, str(e))
  1268. @router.delete("/qc-tasks/{task_id}")
  1269. async def delete_qc_task_api(
  1270. task_id: str,
  1271. user: dict = Depends(require_admin_or_super_admin),
  1272. db: Session = Depends(get_db),
  1273. ):
  1274. try:
  1275. tenant_no = _get_tenant_no(db, user)
  1276. task_svc.delete_qc_task(db, task_id, tenant_no, user.get("user_id"))
  1277. return success_response(None, message="删除成功")
  1278. except ValueError as e:
  1279. return error_response(400, str(e))
  1280. # ---------- 质检物料库 ----------
  1281. def _maybe_strip_qc_material_amount_fields(
  1282. db: Session, user: dict, rows: Iterable[Dict[str, Any]]
  1283. ) -> None:
  1284. if user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM):
  1285. return
  1286. strip_row_fields(rows, *QC_MATERIAL_AMOUNT_FIELD_NAMES)
  1287. def _user_can_view_qc_material_amount(db: Session, user: dict) -> bool:
  1288. return user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM)
  1289. def _strip_amount_from_write_data(db: Session, user: dict, data: Dict[str, Any]) -> None:
  1290. if _user_can_view_qc_material_amount(db, user):
  1291. return
  1292. for key in ("unit_price", "unitPrice", "amount"):
  1293. data.pop(key, None)
  1294. def _resolve_stock_in_amounts(
  1295. db: Session, user: dict, unit_price: Any, amount: Any
  1296. ) -> tuple:
  1297. if _user_can_view_qc_material_amount(db, user):
  1298. return unit_price, amount
  1299. return 0, 0
  1300. def _maybe_strip_qc_third_party_fee_fields(
  1301. db: Session, user: dict, data: Dict[str, Any]
  1302. ) -> None:
  1303. if user_has_menu_perm(db, user, QC_THIRD_PARTY_VIEW_AMOUNT_PERM):
  1304. return
  1305. strip_row_fields([data], "thirdPartyFeeTotal")
  1306. third_items = data.get("thirdPartyItems")
  1307. if isinstance(third_items, list):
  1308. strip_row_fields(third_items, "feeAmount")
  1309. tasks = data.get("tasks")
  1310. if isinstance(tasks, list):
  1311. for task in tasks:
  1312. if isinstance(task, dict) and (task.get("taskType") or "").strip() == "dsfjc":
  1313. strip_row_fields([task], "feeAmount")
  1314. work_order = data.get("workOrder")
  1315. if isinstance(work_order, dict):
  1316. strip_row_fields([work_order], "thirdPartyFeeTotal")
  1317. wo_tasks = work_order.get("tasks")
  1318. if isinstance(wo_tasks, list):
  1319. for task in wo_tasks:
  1320. if isinstance(task, dict) and (task.get("taskType") or "").strip() == "dsfjc":
  1321. strip_row_fields([task], "feeAmount")
  1322. def _maybe_strip_qc_material_stock_check_detail(db: Session, user: dict, data: Dict[str, Any]) -> None:
  1323. if user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM):
  1324. return
  1325. for key in ("batchLines", "unusedLines"):
  1326. lines = data.get(key)
  1327. if isinstance(lines, list):
  1328. strip_row_fields(lines, *QC_MATERIAL_AMOUNT_FIELD_NAMES)
  1329. class QcMaterialBody(BaseModel):
  1330. model_config = ConfigDict(populate_by_name=True)
  1331. material_code: str = Field(..., alias="materialCode")
  1332. material_name: str = Field(..., alias="materialName")
  1333. spec_model: Optional[str] = Field(None, alias="specModel")
  1334. unit: Optional[str] = None
  1335. warning_qty: Optional[float] = Field(0, alias="warningQty")
  1336. status: str = "1"
  1337. remark: Optional[str] = None
  1338. class QcMaterialUpdateBody(BaseModel):
  1339. model_config = ConfigDict(populate_by_name=True)
  1340. material_code: Optional[str] = Field(None, alias="materialCode")
  1341. material_name: Optional[str] = Field(None, alias="materialName")
  1342. spec_model: Optional[str] = Field(None, alias="specModel")
  1343. unit: Optional[str] = None
  1344. warning_qty: Optional[float] = Field(None, alias="warningQty")
  1345. status: Optional[str] = None
  1346. remark: Optional[str] = None
  1347. class StockInBody(BaseModel):
  1348. model_config = ConfigDict(populate_by_name=True)
  1349. stock_in_no: str = Field(..., alias="stockInNo")
  1350. stock_in_date: str = Field(..., alias="stockInDate")
  1351. expire_date: str = Field(..., alias="expireDate")
  1352. catalog_no: str = Field(..., alias="catalogNo")
  1353. batch_no: str = Field(..., alias="batchNo")
  1354. manufacturer: str
  1355. unit_price: float = Field(..., alias="unitPrice")
  1356. amount: float
  1357. qty: float = Field(..., gt=0)
  1358. remark: Optional[str] = None
  1359. stock_in_time: Optional[str] = Field(None, alias="stockInTime")
  1360. class BatchStockInItem(BaseModel):
  1361. model_config = ConfigDict(populate_by_name=True)
  1362. material_id: str = Field(..., alias="materialId")
  1363. stock_in_no: str = Field(..., alias="stockInNo")
  1364. stock_in_date: str = Field(..., alias="stockInDate")
  1365. expire_date: str = Field(..., alias="expireDate")
  1366. catalog_no: str = Field(..., alias="catalogNo")
  1367. batch_no: str = Field(..., alias="batchNo")
  1368. manufacturer: str
  1369. unit_price: float = Field(..., alias="unitPrice")
  1370. amount: float
  1371. qty: float = Field(..., gt=0)
  1372. remark: Optional[str] = None
  1373. class BatchStockInBody(BaseModel):
  1374. model_config = ConfigDict(populate_by_name=True)
  1375. items: List[BatchStockInItem] = Field(..., min_length=1)
  1376. remark: Optional[str] = None
  1377. stock_in_time: Optional[str] = Field(None, alias="stockInTime")
  1378. class QcMonthTemplateBody(BaseModel):
  1379. model_config = ConfigDict(populate_by_name=True)
  1380. material_id: Optional[str] = Field(None, alias="materialId")
  1381. material_code: Optional[str] = Field(None, alias="materialCode")
  1382. material_name: Optional[str] = Field(None, alias="materialName")
  1383. spec_model: Optional[str] = Field(None, alias="specModel")
  1384. unit: Optional[str] = None
  1385. catalog_no: Optional[str] = Field(None, alias="catalogNo")
  1386. batch_no: Optional[str] = Field(None, alias="batchNo")
  1387. manufacturer: Optional[str] = None
  1388. unit_price: Optional[float] = Field(None, alias="unitPrice")
  1389. amount: Optional[float] = None
  1390. in_qty: Optional[float] = Field(None, alias="inQty")
  1391. expire_date: Optional[str] = Field(None, alias="expireDate")
  1392. order_num: Optional[int] = Field(None, alias="orderNum")
  1393. remark: Optional[str] = None
  1394. class QcMonthTemplatePreviewBody(BaseModel):
  1395. model_config = ConfigDict(populate_by_name=True)
  1396. stock_in_date: str = Field(..., alias="stockInDate")
  1397. class QcMonthTemplateConfirmItem(BaseModel):
  1398. model_config = ConfigDict(populate_by_name=True)
  1399. material_id: Optional[str] = Field(None, alias="materialId")
  1400. material_code: str = Field(..., alias="materialCode")
  1401. material_name: str = Field(..., alias="materialName")
  1402. spec_model: Optional[str] = Field(None, alias="specModel")
  1403. unit: Optional[str] = None
  1404. stock_in_no: str = Field(..., alias="stockInNo")
  1405. expire_date: str = Field(..., alias="expireDate")
  1406. catalog_no: str = Field(..., alias="catalogNo")
  1407. batch_no: str = Field(..., alias="batchNo")
  1408. manufacturer: str
  1409. unit_price: float = Field(..., alias="unitPrice")
  1410. amount: float
  1411. qty: float = Field(..., gt=0)
  1412. remark: Optional[str] = None
  1413. class QcMonthTemplateConfirmBody(BaseModel):
  1414. model_config = ConfigDict(populate_by_name=True)
  1415. stock_in_date: str = Field(..., alias="stockInDate")
  1416. items: List[QcMonthTemplateConfirmItem] = Field(..., min_length=1)
  1417. remark: Optional[str] = None
  1418. stock_in_time: Optional[str] = Field(None, alias="stockInTime")
  1419. class BatchStockOutItem(BaseModel):
  1420. model_config = ConfigDict(populate_by_name=True)
  1421. batch_id: str = Field(..., alias="batchId")
  1422. qty: float = Field(..., gt=0)
  1423. remark: Optional[str] = None
  1424. class BatchStockOutBody(BaseModel):
  1425. model_config = ConfigDict(populate_by_name=True)
  1426. items: List[BatchStockOutItem] = Field(..., min_length=1)
  1427. receiver: Optional[str] = None
  1428. purpose: Optional[str] = None
  1429. remark: Optional[str] = None
  1430. stock_out_time: Optional[str] = Field(None, alias="stockOutTime")
  1431. class BatchDiscardItem(BaseModel):
  1432. model_config = ConfigDict(populate_by_name=True)
  1433. batch_id: str = Field(..., alias="batchId")
  1434. qty: float = Field(..., gt=0)
  1435. usage_unit: Optional[str] = Field(None, alias="usageUnit")
  1436. remark: Optional[str] = None
  1437. class BatchDiscardBody(BaseModel):
  1438. model_config = ConfigDict(populate_by_name=True)
  1439. items: List[BatchDiscardItem] = Field(..., min_length=1)
  1440. discard_reason: str = Field(..., alias="discardReason")
  1441. remark: Optional[str] = None
  1442. discard_time: Optional[str] = Field(None, alias="discardTime")
  1443. class QcMaterialBatchUpdateBody(BaseModel):
  1444. model_config = ConfigDict(populate_by_name=True)
  1445. stock_in_no: Optional[str] = Field(None, alias="stockInNo")
  1446. stock_in_date: Optional[str] = Field(None, alias="stockInDate")
  1447. expire_date: Optional[str] = Field(None, alias="expireDate")
  1448. catalog_no: Optional[str] = Field(None, alias="catalogNo")
  1449. batch_no: Optional[str] = Field(None, alias="batchNo")
  1450. manufacturer: Optional[str] = None
  1451. unit_price: Optional[float] = Field(None, alias="unitPrice")
  1452. amount: Optional[float] = None
  1453. in_qty: Optional[float] = Field(None, alias="inQty")
  1454. remaining_qty: Optional[float] = Field(None, alias="remainingQty")
  1455. remark: Optional[str] = None
  1456. qc_attachment_urls: Optional[str] = Field(None, alias="qcAttachmentUrls")
  1457. material_code: Optional[str] = Field(None, alias="materialCode")
  1458. material_name: Optional[str] = Field(None, alias="materialName")
  1459. spec_model: Optional[str] = Field(None, alias="specModel")
  1460. unit: Optional[str] = None
  1461. warning_qty: Optional[float] = Field(None, alias="warningQty")
  1462. status: Optional[str] = None
  1463. @router.get("/qc-material-batches")
  1464. async def list_qc_material_batches(
  1465. page: int = Query(1, ge=1),
  1466. pageSize: int = Query(10, ge=1, le=100),
  1467. keyword: Optional[str] = None,
  1468. materialId: Optional[str] = None,
  1469. status: Optional[str] = None,
  1470. stockStatus: Optional[str] = None,
  1471. db: Session = Depends(get_db),
  1472. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1473. ):
  1474. try:
  1475. tenant_no = _get_tenant_no(db, user)
  1476. items, total = batch_svc.list_batches(
  1477. db,
  1478. page=page,
  1479. page_size=pageSize,
  1480. keyword=keyword,
  1481. material_id=materialId,
  1482. status=status,
  1483. stock_status=stockStatus,
  1484. tenant_no=tenant_no,
  1485. user_info=user,
  1486. )
  1487. _maybe_strip_qc_material_amount_fields(db, user, items)
  1488. return success_response(_page_payload(items, total, page, pageSize))
  1489. except PermissionError as e:
  1490. return error_response(403, str(e))
  1491. except Exception as e:
  1492. return error_response(400, str(e))
  1493. @router.get("/qc-material-batches/options")
  1494. async def qc_material_batch_options(
  1495. keyword: Optional[str] = None,
  1496. materialId: Optional[str] = None,
  1497. onlyAvailable: bool = Query(True, alias="onlyAvailable"),
  1498. db: Session = Depends(get_db),
  1499. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1500. ):
  1501. try:
  1502. tenant_no = _get_tenant_no(db, user)
  1503. options = batch_svc.batch_options(
  1504. db,
  1505. tenant_no,
  1506. keyword=keyword,
  1507. material_id=materialId,
  1508. only_available=onlyAvailable,
  1509. )
  1510. _maybe_strip_qc_material_amount_fields(db, user, options)
  1511. return success_response(options)
  1512. except Exception as e:
  1513. return error_response(400, str(e))
  1514. @router.get("/qc-material-batches/{batch_id}")
  1515. async def get_qc_material_batch(
  1516. batch_id: str,
  1517. db: Session = Depends(get_db),
  1518. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1519. ):
  1520. try:
  1521. tenant_no = _get_tenant_no(db, user)
  1522. payload = batch_svc.get_batch_detail(db, batch_id, tenant_no=tenant_no)
  1523. _maybe_strip_qc_material_amount_fields(db, user, [payload])
  1524. return success_response(payload)
  1525. except ValueError as e:
  1526. return error_response(400, str(e))
  1527. except Exception as e:
  1528. return error_response(400, str(e))
  1529. @router.put("/qc-material-batches/{batch_id}")
  1530. async def update_qc_material_batch(
  1531. batch_id: str,
  1532. body: QcMaterialBatchUpdateBody,
  1533. db: Session = Depends(get_db),
  1534. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1535. ):
  1536. try:
  1537. tenant_no = _get_tenant_no(db, user)
  1538. user_id = _get_current_user_id(user)
  1539. data = body.model_dump(exclude_none=True)
  1540. _strip_amount_from_write_data(db, user, data)
  1541. obj = batch_svc.update_batch(
  1542. db,
  1543. batch_id,
  1544. tenant_no=tenant_no,
  1545. user_id=user_id,
  1546. **data,
  1547. )
  1548. material = mat_svc.get_material(db, obj.material_id, tenant_no=tenant_no)
  1549. payload = batch_svc._batch_row_to_dict(obj, material)
  1550. _maybe_strip_qc_material_amount_fields(db, user, [payload])
  1551. return success_response(
  1552. payload,
  1553. message="更新成功",
  1554. )
  1555. except ValueError as e:
  1556. return error_response(400, str(e))
  1557. except Exception as e:
  1558. return error_response(400, str(e))
  1559. @router.delete("/qc-material-batches/{batch_id}")
  1560. async def delete_qc_material_batch(
  1561. batch_id: str,
  1562. db: Session = Depends(get_db),
  1563. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1564. ):
  1565. try:
  1566. tenant_no = _get_tenant_no(db, user)
  1567. user_id = _get_current_user_id(user)
  1568. batch_svc.delete_batch(db, batch_id, tenant_no=tenant_no, user_id=user_id)
  1569. return success_response(None, message="删除成功")
  1570. except ValueError as e:
  1571. return error_response(400, str(e))
  1572. except Exception as e:
  1573. return error_response(400, str(e))
  1574. @router.get("/qc-materials")
  1575. async def list_qc_materials(
  1576. page: int = Query(1, ge=1),
  1577. pageSize: int = Query(10, ge=1, le=100),
  1578. keyword: Optional[str] = None,
  1579. status: Optional[str] = None,
  1580. db: Session = Depends(get_db),
  1581. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1582. ):
  1583. try:
  1584. tenant_no = _get_tenant_no(db, user)
  1585. items, total = mat_svc.list_materials(
  1586. db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
  1587. user_info=user,
  1588. )
  1589. return success_response(_page_payload(items, total, page, pageSize))
  1590. except PermissionError as e:
  1591. return error_response(403, str(e))
  1592. except Exception as e:
  1593. return error_response(400, str(e))
  1594. @router.get("/qc-materials/options")
  1595. async def qc_material_options(
  1596. db: Session = Depends(get_db),
  1597. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1598. ):
  1599. try:
  1600. tenant_no = _get_tenant_no(db, user)
  1601. return success_response(mat_svc.options_list(db, tenant_no=tenant_no))
  1602. except Exception as e:
  1603. return error_response(400, str(e))
  1604. @router.post("/qc-materials")
  1605. async def create_qc_material(
  1606. body: QcMaterialBody,
  1607. db: Session = Depends(get_db),
  1608. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1609. ):
  1610. try:
  1611. tenant_no = _get_tenant_no(db, user)
  1612. user_id = _get_current_user_id(user)
  1613. obj = mat_svc.create_material(
  1614. db,
  1615. material_code=body.material_code,
  1616. material_name=body.material_name,
  1617. spec_model=body.spec_model,
  1618. unit=body.unit,
  1619. warning_qty=Decimal(str(body.warning_qty))
  1620. if body.warning_qty is not None
  1621. else None,
  1622. status=mat_svc._norm_status(body.status),
  1623. remark=body.remark,
  1624. tenant_no=tenant_no,
  1625. user_id=user_id,
  1626. )
  1627. return success_response(mat_svc._row_to_dict(obj), message="创建成功")
  1628. except ValueError as e:
  1629. return error_response(400, str(e))
  1630. except Exception as e:
  1631. return error_response(400, str(e))
  1632. @router.put("/qc-materials/{material_id}")
  1633. async def update_qc_material(
  1634. material_id: str,
  1635. body: QcMaterialUpdateBody,
  1636. db: Session = Depends(get_db),
  1637. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1638. ):
  1639. try:
  1640. tenant_no = _get_tenant_no(db, user)
  1641. user_id = _get_current_user_id(user)
  1642. data = body.model_dump(exclude_none=True)
  1643. if "warning_qty" in data:
  1644. data["warning_qty"] = Decimal(str(data["warning_qty"]))
  1645. if "status" in data:
  1646. data["status"] = mat_svc._norm_status(data["status"])
  1647. obj = mat_svc.update_material(
  1648. db,
  1649. material_id,
  1650. tenant_no=tenant_no,
  1651. user_id=user_id,
  1652. **data,
  1653. )
  1654. return success_response(mat_svc._row_to_dict(obj), message="更新成功")
  1655. except ValueError as e:
  1656. return error_response(400, str(e))
  1657. except Exception as e:
  1658. return error_response(400, str(e))
  1659. @router.delete("/qc-materials/{material_id}")
  1660. async def delete_qc_material(
  1661. material_id: str,
  1662. db: Session = Depends(get_db),
  1663. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1664. ):
  1665. try:
  1666. tenant_no = _get_tenant_no(db, user)
  1667. user_id = _get_current_user_id(user)
  1668. mat_svc.soft_delete_material(db, material_id, tenant_no=tenant_no)
  1669. return success_response(None, message="删除成功")
  1670. except ValueError as e:
  1671. return error_response(400, str(e))
  1672. except Exception as e:
  1673. return error_response(400, str(e))
  1674. @router.post("/qc-materials/batch-stock-in")
  1675. async def batch_stock_in_qc_material(
  1676. body: BatchStockInBody,
  1677. db: Session = Depends(get_db),
  1678. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1679. ):
  1680. try:
  1681. tenant_no = _get_tenant_no(db, user)
  1682. user_id = _get_current_user_id(user)
  1683. operate_time = datetime.now()
  1684. if body.stock_in_time:
  1685. rt = parse_datetime(body.stock_in_time)
  1686. if not rt:
  1687. return error_response(400, "入库时间格式无效")
  1688. operate_time = rt
  1689. items = []
  1690. for x in body.items:
  1691. unit_price, amount = _resolve_stock_in_amounts(
  1692. db, user, x.unit_price, x.amount
  1693. )
  1694. items.append(
  1695. {
  1696. "material_id": x.material_id,
  1697. "materialId": x.material_id,
  1698. "stock_in_no": x.stock_in_no,
  1699. "stockInNo": x.stock_in_no,
  1700. "stock_in_date": x.stock_in_date,
  1701. "stockInDate": x.stock_in_date,
  1702. "expire_date": x.expire_date,
  1703. "expireDate": x.expire_date,
  1704. "catalog_no": x.catalog_no,
  1705. "catalogNo": x.catalog_no,
  1706. "batch_no": x.batch_no,
  1707. "batchNo": x.batch_no,
  1708. "manufacturer": x.manufacturer,
  1709. "unit_price": unit_price,
  1710. "unitPrice": unit_price,
  1711. "amount": amount,
  1712. "qty": x.qty,
  1713. "remark": x.remark,
  1714. }
  1715. )
  1716. count = batch_svc.batch_stock_in(
  1717. db,
  1718. items=items,
  1719. remark=body.remark,
  1720. tenant_no=tenant_no,
  1721. user_id=user_id,
  1722. operate_time=operate_time,
  1723. )
  1724. return success_response({"count": count}, message=f"批量入库成功,共 {count} 项")
  1725. except ValueError as e:
  1726. return error_response(400, str(e))
  1727. except Exception as e:
  1728. return error_response(400, str(e))
  1729. @router.post("/qc-materials/batch-stock-out")
  1730. async def batch_stock_out_qc_material(
  1731. body: BatchStockOutBody,
  1732. db: Session = Depends(get_db),
  1733. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1734. ):
  1735. try:
  1736. tenant_no = _get_tenant_no(db, user)
  1737. user_id = _get_current_user_id(user)
  1738. operate_time = datetime.now()
  1739. if body.stock_out_time:
  1740. rt = parse_datetime(body.stock_out_time)
  1741. if not rt:
  1742. return error_response(400, "出库时间格式无效")
  1743. operate_time = rt
  1744. items = [
  1745. {"batch_id": x.batch_id, "batchId": x.batch_id, "qty": x.qty, "remark": x.remark}
  1746. for x in body.items
  1747. ]
  1748. count = batch_svc.batch_stock_out(
  1749. db,
  1750. items=items,
  1751. receiver=body.receiver,
  1752. purpose=body.purpose,
  1753. remark=body.remark,
  1754. tenant_no=tenant_no,
  1755. user_id=user_id,
  1756. operate_time=operate_time,
  1757. )
  1758. return success_response({"count": count}, message=f"批量出库成功,共 {count} 项")
  1759. except ValueError as e:
  1760. return error_response(400, str(e))
  1761. except Exception as e:
  1762. return error_response(400, str(e))
  1763. @router.post("/qc-materials/batch-discard")
  1764. async def batch_discard_qc_material(
  1765. body: BatchDiscardBody,
  1766. db: Session = Depends(get_db),
  1767. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1768. ):
  1769. try:
  1770. tenant_no = _get_tenant_no(db, user)
  1771. user_id = _get_current_user_id(user)
  1772. operate_time = datetime.now()
  1773. if body.discard_time:
  1774. rt = parse_datetime(body.discard_time)
  1775. if not rt:
  1776. return error_response(400, "废弃时间格式无效")
  1777. operate_time = rt
  1778. items = [
  1779. {
  1780. "batch_id": x.batch_id,
  1781. "batchId": x.batch_id,
  1782. "qty": x.qty,
  1783. "usageUnit": x.usage_unit,
  1784. "usage_unit": x.usage_unit,
  1785. "remark": x.remark,
  1786. }
  1787. for x in body.items
  1788. ]
  1789. count = batch_svc.batch_discard(
  1790. db,
  1791. items=items,
  1792. discard_reason=body.discard_reason,
  1793. remark=body.remark,
  1794. tenant_no=tenant_no,
  1795. user_id=user_id,
  1796. operate_time=operate_time,
  1797. )
  1798. return success_response({"count": count}, message=f"批量废弃成功,共 {count} 项")
  1799. except ValueError as e:
  1800. return error_response(400, str(e))
  1801. except Exception as e:
  1802. return error_response(400, str(e))
  1803. @router.post("/qc-materials/{material_id}/stock-in")
  1804. async def stock_in_qc_material(
  1805. material_id: str,
  1806. body: StockInBody,
  1807. db: Session = Depends(get_db),
  1808. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1809. ):
  1810. try:
  1811. tenant_no = _get_tenant_no(db, user)
  1812. user_id = _get_current_user_id(user)
  1813. operate_time = datetime.now()
  1814. if body.stock_in_time:
  1815. rt = parse_datetime(body.stock_in_time)
  1816. if not rt:
  1817. return error_response(400, "入库时间格式无效")
  1818. operate_time = rt
  1819. from decimal import Decimal
  1820. unit_price, amount = _resolve_stock_in_amounts(
  1821. db, user, body.unit_price, body.amount
  1822. )
  1823. obj = batch_svc.stock_in_one(
  1824. db,
  1825. material_id,
  1826. stock_in_no=body.stock_in_no,
  1827. stock_in_date=body.stock_in_date,
  1828. expire_date=body.expire_date,
  1829. catalog_no=body.catalog_no,
  1830. batch_no=body.batch_no,
  1831. manufacturer=body.manufacturer,
  1832. unit_price=unit_price,
  1833. amount=amount,
  1834. qty=Decimal(str(body.qty)),
  1835. remark=body.remark,
  1836. tenant_no=tenant_no,
  1837. user_id=user_id,
  1838. operate_time=operate_time,
  1839. )
  1840. return success_response(batch_svc._batch_row_to_dict(obj), message="入库成功")
  1841. except ValueError as e:
  1842. return error_response(400, str(e))
  1843. except Exception as e:
  1844. return error_response(400, str(e))
  1845. @router.post("/qc-material-batches/batch-stock-in")
  1846. async def batch_stock_in_qc_material_batches(
  1847. body: BatchStockInBody,
  1848. db: Session = Depends(get_db),
  1849. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1850. ):
  1851. """批量入库(与 /qc-materials/batch-stock-in 等价)。"""
  1852. return await batch_stock_in_qc_material(body, db, user)
  1853. @router.post("/qc-material-batches/batch-stock-out")
  1854. async def batch_stock_out_qc_material_batches(
  1855. body: BatchStockOutBody,
  1856. db: Session = Depends(get_db),
  1857. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1858. ):
  1859. """批量出库(与 /qc-materials/batch-stock-out 等价)。"""
  1860. return await batch_stock_out_qc_material(body, db, user)
  1861. @router.post("/qc-material-batches/batch-discard")
  1862. async def batch_discard_qc_material_batches(
  1863. body: BatchDiscardBody,
  1864. db: Session = Depends(get_db),
  1865. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1866. ):
  1867. """批量废弃(与 /qc-materials/batch-discard 等价)。"""
  1868. return await batch_discard_qc_material(body, db, user)
  1869. # ---------- 月初入库模板 ----------
  1870. @router.get("/qc-material-month-templates")
  1871. async def list_qc_month_templates(
  1872. db: Session = Depends(get_db),
  1873. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1874. ):
  1875. try:
  1876. tenant_no = _get_tenant_no(db, user)
  1877. rows = month_tpl_svc.list_templates(db, tenant_no=tenant_no)
  1878. return success_response(rows)
  1879. except ValueError as e:
  1880. return error_response(400, str(e))
  1881. except Exception as e:
  1882. return error_response(400, str(e))
  1883. @router.post("/qc-material-month-templates")
  1884. async def create_qc_month_template(
  1885. body: QcMonthTemplateBody,
  1886. db: Session = Depends(get_db),
  1887. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1888. ):
  1889. try:
  1890. tenant_no = _get_tenant_no(db, user)
  1891. user_id = _get_current_user_id(user)
  1892. unit_price, amount = _resolve_stock_in_amounts(
  1893. db, user, body.unit_price, body.amount
  1894. )
  1895. data = body.model_dump(by_alias=False)
  1896. data["unit_price"] = unit_price if unit_price is not None else 0
  1897. data["amount"] = amount if amount is not None else 0
  1898. row = month_tpl_svc.create_template(
  1899. db, data=data, tenant_no=tenant_no, user_id=user_id
  1900. )
  1901. return success_response(row, message="新增成功")
  1902. except ValueError as e:
  1903. return error_response(400, str(e))
  1904. except Exception as e:
  1905. return error_response(400, str(e))
  1906. @router.put("/qc-material-month-templates/{row_id}")
  1907. async def update_qc_month_template(
  1908. row_id: str,
  1909. body: QcMonthTemplateBody,
  1910. db: Session = Depends(get_db),
  1911. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1912. ):
  1913. try:
  1914. tenant_no = _get_tenant_no(db, user)
  1915. user_id = _get_current_user_id(user)
  1916. unit_price, amount = _resolve_stock_in_amounts(
  1917. db, user, body.unit_price, body.amount
  1918. )
  1919. data = body.model_dump(by_alias=False)
  1920. data["unit_price"] = unit_price if unit_price is not None else 0
  1921. data["amount"] = amount if amount is not None else 0
  1922. row = month_tpl_svc.update_template(
  1923. db, row_id, data=data, tenant_no=tenant_no, user_id=user_id
  1924. )
  1925. return success_response(row, message="更新成功")
  1926. except ValueError as e:
  1927. return error_response(400, str(e))
  1928. except Exception as e:
  1929. return error_response(400, str(e))
  1930. @router.delete("/qc-material-month-templates/{row_id}")
  1931. async def delete_qc_month_template(
  1932. row_id: str,
  1933. db: Session = Depends(get_db),
  1934. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1935. ):
  1936. try:
  1937. tenant_no = _get_tenant_no(db, user)
  1938. month_tpl_svc.soft_delete_template(db, row_id, tenant_no=tenant_no)
  1939. return success_response(None, message="删除成功")
  1940. except ValueError as e:
  1941. return error_response(400, str(e))
  1942. except Exception as e:
  1943. return error_response(400, str(e))
  1944. @router.post("/qc-material-month-templates/preview")
  1945. async def preview_qc_month_templates(
  1946. body: QcMonthTemplatePreviewBody,
  1947. db: Session = Depends(get_db),
  1948. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1949. ):
  1950. try:
  1951. tenant_no = _get_tenant_no(db, user)
  1952. stock_in_date = _parse_date(body.stock_in_date)
  1953. rows = month_tpl_svc.preview_templates(
  1954. db, tenant_no=tenant_no, stock_in_date=stock_in_date
  1955. )
  1956. return success_response(rows)
  1957. except ValueError as e:
  1958. return error_response(400, str(e))
  1959. except Exception as e:
  1960. return error_response(400, str(e))
  1961. @router.post("/qc-material-month-templates/confirm")
  1962. async def confirm_qc_month_templates(
  1963. body: QcMonthTemplateConfirmBody,
  1964. db: Session = Depends(get_db),
  1965. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  1966. ):
  1967. try:
  1968. tenant_no = _get_tenant_no(db, user)
  1969. user_id = _get_current_user_id(user)
  1970. operate_time = datetime.now()
  1971. if body.stock_in_time:
  1972. rt = parse_datetime(body.stock_in_time)
  1973. if not rt:
  1974. return error_response(400, "入库时间格式无效")
  1975. operate_time = rt
  1976. stock_in_date = _parse_date(body.stock_in_date)
  1977. items = []
  1978. for x in body.items:
  1979. unit_price, amount = _resolve_stock_in_amounts(
  1980. db, user, x.unit_price, x.amount
  1981. )
  1982. items.append(
  1983. {
  1984. "material_id": x.material_id,
  1985. "materialId": x.material_id,
  1986. "material_code": x.material_code,
  1987. "materialCode": x.material_code,
  1988. "material_name": x.material_name,
  1989. "materialName": x.material_name,
  1990. "spec_model": x.spec_model,
  1991. "specModel": x.spec_model,
  1992. "unit": x.unit,
  1993. "stock_in_no": x.stock_in_no,
  1994. "stockInNo": x.stock_in_no,
  1995. "expire_date": x.expire_date,
  1996. "expireDate": x.expire_date,
  1997. "catalog_no": x.catalog_no,
  1998. "catalogNo": x.catalog_no,
  1999. "batch_no": x.batch_no,
  2000. "batchNo": x.batch_no,
  2001. "manufacturer": x.manufacturer,
  2002. "unit_price": unit_price,
  2003. "unitPrice": unit_price,
  2004. "amount": amount,
  2005. "qty": x.qty,
  2006. "remark": x.remark,
  2007. }
  2008. )
  2009. count = month_tpl_svc.confirm_month_import(
  2010. db,
  2011. tenant_no=tenant_no,
  2012. user_id=user_id,
  2013. stock_in_date=stock_in_date,
  2014. items=items,
  2015. remark=body.remark,
  2016. operate_time=operate_time,
  2017. )
  2018. return success_response({"count": count}, message=f"月初导入成功,共 {count} 项")
  2019. except ValueError as e:
  2020. return error_response(400, str(e))
  2021. except Exception as e:
  2022. return error_response(400, str(e))
  2023. # ---------- 物料变动记录 ----------
  2024. @router.get("/qc-material-usages")
  2025. async def list_qc_material_usages(
  2026. page: int = Query(1, ge=1),
  2027. pageSize: int = Query(10, ge=1, le=100),
  2028. materialId: Optional[str] = None,
  2029. changeType: Optional[str] = None,
  2030. startTime: Optional[str] = None,
  2031. endTime: Optional[str] = None,
  2032. keyword: Optional[str] = None,
  2033. stockInNo: Optional[str] = None,
  2034. db: Session = Depends(get_db),
  2035. user: dict = Depends(require_menu_code("biz:qcMaterialUsage:list")),
  2036. ):
  2037. try:
  2038. tenant_no = _get_tenant_no(db, user)
  2039. items, total = mu_svc.list_usages(
  2040. db, tenant_no, page=page, page_size=pageSize,
  2041. material_id=materialId, change_type=changeType,
  2042. start_time=startTime, end_time=endTime,
  2043. keyword=keyword, stock_in_no=stockInNo,
  2044. user_info=user,
  2045. )
  2046. _maybe_strip_qc_material_amount_fields(db, user, items)
  2047. return success_response(_page_payload(items, total, page, pageSize))
  2048. except PermissionError as e:
  2049. return error_response(403, str(e))
  2050. except Exception as e:
  2051. return error_response(400, str(e))
  2052. @router.get("/my-qc-material-usages")
  2053. async def list_my_qc_material_usages(
  2054. page: int = Query(1, ge=1),
  2055. pageSize: int = Query(10, ge=1, le=100),
  2056. materialId: Optional[str] = None,
  2057. changeType: Optional[str] = None,
  2058. startTime: Optional[str] = None,
  2059. endTime: Optional[str] = None,
  2060. keyword: Optional[str] = None,
  2061. stockInNo: Optional[str] = None,
  2062. db: Session = Depends(get_db),
  2063. user: dict = Depends(require_menu_code("biz:myQcMaterialUsage:list")),
  2064. ):
  2065. """当前登录人的物料领用记录(手动出库领用人 / 任务使用填报人)。"""
  2066. try:
  2067. tenant_no = _get_tenant_no(db, user)
  2068. items, total = mu_svc.list_my_usages(
  2069. db,
  2070. tenant_no,
  2071. user_id=_get_current_user_id(user),
  2072. nick_name=(user.get("real_name") or "").strip(),
  2073. page=page,
  2074. page_size=pageSize,
  2075. material_id=materialId,
  2076. change_type=changeType,
  2077. start_time=startTime,
  2078. end_time=endTime,
  2079. keyword=keyword,
  2080. stock_in_no=stockInNo,
  2081. )
  2082. if not user_has_menu_perm(db, user, MY_QC_MATERIAL_USAGE_VIEW_AMOUNT_PERM):
  2083. strip_row_fields(items, "unitPrice", "amount")
  2084. return success_response(_page_payload(items, total, page, pageSize))
  2085. except PermissionError as e:
  2086. return error_response(403, str(e))
  2087. except Exception as e:
  2088. return error_response(400, str(e))
  2089. @router.get("/qc-material-usages/{usage_id}/linked-task-usages")
  2090. async def list_qc_material_linked_task_usages(
  2091. usage_id: str,
  2092. db: Session = Depends(get_db),
  2093. user: dict = Depends(
  2094. require_any_menu_code("biz:qcMaterialUsage:list", "biz:myQcMaterialUsage:list")
  2095. ),
  2096. ):
  2097. """领用记录下关联的质检任务物料消耗明细。"""
  2098. try:
  2099. tenant_no = _get_tenant_no(db, user)
  2100. items = mu_svc.list_linked_task_usages(
  2101. db, usage_id, tenant_no, user_info=user
  2102. )
  2103. if not (
  2104. user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM)
  2105. or user_has_menu_perm(db, user, MY_QC_MATERIAL_USAGE_VIEW_AMOUNT_PERM)
  2106. ):
  2107. strip_row_fields(items, *QC_MATERIAL_AMOUNT_FIELD_NAMES)
  2108. return success_response(items)
  2109. except HTTPException as e:
  2110. detail = e.detail
  2111. msg = detail if isinstance(detail, str) else str(detail)
  2112. return error_response(e.status_code, msg)
  2113. except PermissionError as e:
  2114. return error_response(403, str(e))
  2115. except Exception as e:
  2116. return error_response(400, str(e))
  2117. @router.get("/qc-material-stock-check-requisitions")
  2118. async def list_qc_material_stock_check_requisitions(
  2119. page: int = Query(1, ge=1),
  2120. pageSize: int = Query(50, ge=1, le=200),
  2121. keyword: Optional[str] = None,
  2122. db: Session = Depends(get_db),
  2123. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  2124. ):
  2125. """盘点登记:待处理领用记录(未标记已使用完)。"""
  2126. try:
  2127. tenant_no = _get_tenant_no(db, user)
  2128. items, total = mu_svc.list_stock_check_requisitions(
  2129. db,
  2130. tenant_no,
  2131. page=page,
  2132. page_size=pageSize,
  2133. keyword=keyword,
  2134. user_info=user,
  2135. )
  2136. _maybe_strip_qc_material_amount_fields(db, user, items)
  2137. return success_response(_page_payload(items, total, page, pageSize))
  2138. except PermissionError as e:
  2139. return error_response(403, str(e))
  2140. except Exception as e:
  2141. return error_response(400, str(e))
  2142. @router.post("/qc-material-usages/{usage_id}/mark-fully-used")
  2143. async def mark_qc_material_usage_fully_used(
  2144. usage_id: str,
  2145. db: Session = Depends(get_db),
  2146. user: dict = Depends(
  2147. require_any_menu_code("biz:qcMaterial:list", "biz:qcMaterialUsage:list")
  2148. ),
  2149. ):
  2150. """标记领用记录已使用完(盘点登记 / 变动记录)。"""
  2151. try:
  2152. tenant_no = _get_tenant_no(db, user)
  2153. user_id = _get_current_user_id(user)
  2154. mu_svc.mark_requisition_fully_used(
  2155. db,
  2156. usage_id,
  2157. tenant_no=tenant_no,
  2158. user_id=user_id,
  2159. user_info=user,
  2160. )
  2161. return success_response(None, message="已标记为使用完")
  2162. except PermissionError as e:
  2163. return error_response(403, str(e))
  2164. except ValueError as e:
  2165. return error_response(400, str(e))
  2166. except Exception as e:
  2167. return error_response(400, str(e))
  2168. # ---------- 库存盘点 ----------
  2169. class StockCheckCreateBody(BaseModel):
  2170. model_config = ConfigDict(populate_by_name=True)
  2171. title: str
  2172. check_time: Optional[str] = Field(None, alias="checkTime")
  2173. remark: Optional[str] = None
  2174. class StockCheckUnusedLineItem(BaseModel):
  2175. model_config = ConfigDict(populate_by_name=True)
  2176. usage_id: Optional[str] = Field(None, alias="usageId")
  2177. batch_id: Optional[str] = Field(None, alias="batchId")
  2178. material_code: Optional[str] = Field(None, alias="materialCode")
  2179. material_name: str = Field(..., alias="materialName")
  2180. spec_model: Optional[str] = Field(None, alias="specModel")
  2181. unit: Optional[str] = None
  2182. stock_in_no: Optional[str] = Field(None, alias="stockInNo")
  2183. stock_in_date: Optional[str] = Field(None, alias="stockInDate")
  2184. expire_date: Optional[str] = Field(None, alias="expireDate")
  2185. catalog_no: Optional[str] = Field(None, alias="catalogNo")
  2186. batch_no: Optional[str] = Field(None, alias="batchNo")
  2187. manufacturer: Optional[str] = None
  2188. unit_price: Optional[float] = Field(None, alias="unitPrice")
  2189. holder: Optional[str] = None
  2190. unused_qty: float = Field(..., alias="unusedQty", gt=0)
  2191. remark: Optional[str] = None
  2192. class StockCheckUnusedLinesBody(BaseModel):
  2193. model_config = ConfigDict(populate_by_name=True)
  2194. items: List[StockCheckUnusedLineItem] = Field(default_factory=list)
  2195. @router.get("/qc-material-stock-checks")
  2196. async def list_qc_material_stock_checks(
  2197. page: int = Query(1, ge=1),
  2198. pageSize: int = Query(10, ge=1, le=100),
  2199. keyword: Optional[str] = None,
  2200. status: Optional[str] = None,
  2201. startTime: Optional[str] = None,
  2202. endTime: Optional[str] = None,
  2203. db: Session = Depends(get_db),
  2204. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  2205. ):
  2206. try:
  2207. tenant_no = _get_tenant_no(db, user)
  2208. items, total = stock_check_svc.list_checks(
  2209. db,
  2210. tenant_no,
  2211. page=page,
  2212. page_size=pageSize,
  2213. keyword=keyword,
  2214. status=status,
  2215. start_time=startTime,
  2216. end_time=endTime,
  2217. user_info=user,
  2218. )
  2219. return success_response(_page_payload(items, total, page, pageSize))
  2220. except PermissionError as e:
  2221. return error_response(403, str(e))
  2222. except Exception as e:
  2223. return error_response(400, str(e))
  2224. @router.post("/qc-material-stock-checks")
  2225. async def create_qc_material_stock_check(
  2226. body: StockCheckCreateBody,
  2227. db: Session = Depends(get_db),
  2228. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  2229. ):
  2230. try:
  2231. tenant_no = _get_tenant_no(db, user)
  2232. user_id = _get_current_user_id(user)
  2233. dept_id = user.get("deptId") or user.get("dept_id") or ""
  2234. res = stock_check_svc.create_check(
  2235. db,
  2236. title=body.title,
  2237. tenant_no=tenant_no,
  2238. user_id=user_id,
  2239. dept_id=dept_id,
  2240. check_time=body.check_time,
  2241. remark=body.remark,
  2242. )
  2243. return success_response(res, message="盘点单创建成功")
  2244. except ValueError as e:
  2245. return error_response(400, str(e))
  2246. except Exception as e:
  2247. return error_response(400, str(e))
  2248. @router.get("/qc-material-stock-checks/{check_id}")
  2249. async def get_qc_material_stock_check(
  2250. check_id: str,
  2251. db: Session = Depends(get_db),
  2252. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  2253. ):
  2254. try:
  2255. tenant_no = _get_tenant_no(db, user)
  2256. data = stock_check_svc.get_check_detail(db, check_id, tenant_no)
  2257. _maybe_strip_qc_material_stock_check_detail(db, user, data)
  2258. return success_response(data)
  2259. except ValueError as e:
  2260. return error_response(400, str(e))
  2261. except Exception as e:
  2262. return error_response(400, str(e))
  2263. @router.put("/qc-material-stock-checks/{check_id}/unused-lines")
  2264. async def save_qc_material_stock_check_unused_lines(
  2265. check_id: str,
  2266. body: StockCheckUnusedLinesBody,
  2267. db: Session = Depends(get_db),
  2268. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  2269. ):
  2270. try:
  2271. tenant_no = _get_tenant_no(db, user)
  2272. user_id = _get_current_user_id(user)
  2273. dept_id = user.get("deptId") or user.get("dept_id") or ""
  2274. count = stock_check_svc.save_unused_lines(
  2275. db,
  2276. check_id,
  2277. items=[x.model_dump(by_alias=False) for x in body.items],
  2278. tenant_no=tenant_no,
  2279. user_id=user_id,
  2280. dept_id=dept_id,
  2281. )
  2282. return success_response({"count": count}, message="登记保存成功")
  2283. except ValueError as e:
  2284. return error_response(400, str(e))
  2285. except Exception as e:
  2286. return error_response(400, str(e))
  2287. @router.post("/qc-material-stock-checks/{check_id}/complete")
  2288. async def complete_qc_material_stock_check(
  2289. check_id: str,
  2290. db: Session = Depends(get_db),
  2291. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  2292. ):
  2293. try:
  2294. tenant_no = _get_tenant_no(db, user)
  2295. user_id = _get_current_user_id(user)
  2296. stock_check_svc.complete_check(db, check_id, tenant_no=tenant_no, user_id=user_id)
  2297. return success_response(None, message="盘点已完成")
  2298. except ValueError as e:
  2299. return error_response(400, str(e))
  2300. except Exception as e:
  2301. return error_response(400, str(e))
  2302. @router.post("/qc-material-stock-checks/{check_id}/cancel")
  2303. async def cancel_qc_material_stock_check(
  2304. check_id: str,
  2305. db: Session = Depends(get_db),
  2306. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  2307. ):
  2308. try:
  2309. tenant_no = _get_tenant_no(db, user)
  2310. user_id = _get_current_user_id(user)
  2311. stock_check_svc.cancel_check(db, check_id, tenant_no=tenant_no, user_id=user_id)
  2312. return success_response(None, message="盘点已作废")
  2313. except ValueError as e:
  2314. return error_response(400, str(e))
  2315. except Exception as e:
  2316. return error_response(400, str(e))
  2317. # ---------- 质检物料金额查看授权 ----------
  2318. class QcMaterialAmountAuthGrantBody(BaseModel):
  2319. model_config = ConfigDict(populate_by_name=True)
  2320. user_id: str = Field(..., alias="userId")
  2321. class QcMaterialAmountAuthBatchBody(BaseModel):
  2322. model_config = ConfigDict(populate_by_name=True)
  2323. user_ids: List[str] = Field(..., alias="userIds", min_length=1)
  2324. @router.get("/qc-material-amount-auth/users")
  2325. async def list_qc_material_amount_auth_users(
  2326. page: int = Query(1, ge=1),
  2327. pageSize: int = Query(10, ge=1, le=100),
  2328. keyword: Optional[str] = None,
  2329. db: Session = Depends(get_db),
  2330. user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
  2331. ):
  2332. """已授权「质检物料金额查看」角色的用户列表。"""
  2333. try:
  2334. tenant_no = _get_tenant_no(db, user)
  2335. items, total = amount_auth_svc.list_authorized_users(
  2336. db,
  2337. tenant_no=tenant_no,
  2338. page=page,
  2339. page_size=pageSize,
  2340. keyword=keyword,
  2341. )
  2342. return success_response(_page_payload(items, total, page, pageSize))
  2343. except Exception as e:
  2344. return error_response(400, str(e))
  2345. @router.post("/qc-material-amount-auth/users")
  2346. async def grant_qc_material_amount_auth(
  2347. body: QcMaterialAmountAuthGrantBody,
  2348. db: Session = Depends(get_db),
  2349. user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
  2350. ):
  2351. """为用户绑定「质检物料金额查看」专用角色。"""
  2352. try:
  2353. tenant_no = _get_tenant_no(db, user)
  2354. operator_id = _get_current_user_id(user)
  2355. created = amount_auth_svc.grant_amount_view_role(
  2356. db,
  2357. tenant_no=tenant_no,
  2358. user_id=body.user_id.strip(),
  2359. operator_id=operator_id,
  2360. )
  2361. msg = "授权成功" if created else "该用户已授权"
  2362. return success_response({"created": created}, message=msg)
  2363. except ValueError as e:
  2364. return error_response(400, str(e))
  2365. except Exception as e:
  2366. return error_response(400, str(e))
  2367. @router.delete("/qc-material-amount-auth/users/{user_id}")
  2368. async def revoke_qc_material_amount_auth(
  2369. user_id: str,
  2370. db: Session = Depends(get_db),
  2371. user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
  2372. ):
  2373. """解除用户的「质检物料金额查看」专用角色。"""
  2374. try:
  2375. tenant_no = _get_tenant_no(db, user)
  2376. operator_id = _get_current_user_id(user)
  2377. removed = amount_auth_svc.revoke_amount_view_role(
  2378. db,
  2379. tenant_no=tenant_no,
  2380. user_id=user_id.strip(),
  2381. operator_id=operator_id,
  2382. )
  2383. if not removed:
  2384. return error_response(400, "该用户未授权或已解除")
  2385. return success_response(None, message="已解除授权")
  2386. except ValueError as e:
  2387. return error_response(400, str(e))
  2388. except Exception as e:
  2389. return error_response(400, str(e))
  2390. @router.post("/qc-material-amount-auth/users/batch-revoke")
  2391. async def batch_revoke_qc_material_amount_auth(
  2392. body: QcMaterialAmountAuthBatchBody,
  2393. db: Session = Depends(get_db),
  2394. user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
  2395. ):
  2396. """批量解除「质检物料金额查看」授权。"""
  2397. try:
  2398. tenant_no = _get_tenant_no(db, user)
  2399. operator_id = _get_current_user_id(user)
  2400. count = 0
  2401. for uid in body.user_ids:
  2402. uid = (uid or "").strip()
  2403. if not uid:
  2404. continue
  2405. if amount_auth_svc.revoke_amount_view_role(
  2406. db,
  2407. tenant_no=tenant_no,
  2408. user_id=uid,
  2409. operator_id=operator_id,
  2410. ):
  2411. count += 1
  2412. return success_response({"count": count}, message=f"已解除 {count} 项授权")
  2413. except ValueError as e:
  2414. return error_response(400, str(e))
  2415. except Exception as e:
  2416. return error_response(400, str(e))