qc.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  1. """质量检测 — 第三方/实验室主数据、质检工单、质检任务 API"""
  2. from typing import Any, Dict, 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 require_admin_or_super_admin, require_menu_code
  10. from app.core.response import error_response, success_response
  11. from app.core.utils import parse_datetime
  12. from app.crud import qc_lab_test_item as lab_svc
  13. from app.crud import qc_item_param as param_svc
  14. from app.crud import qc_task_result as result_svc
  15. from app.crud import qc_task as task_svc
  16. from app.crud import qc_third_party_company as tpc_svc
  17. from app.crud import qc_third_party_item as tpi_svc
  18. from app.crud import qc_material as mat_svc
  19. from app.crud import qc_material_usage as mu_svc
  20. from app.crud import qc_work_order as wo_svc
  21. from app.crud.collection_protocol import total_pages
  22. from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload
  23. router = APIRouter(prefix="/web/admin", tags=["质量检测"])
  24. # ---------- 第三方检测公司 ----------
  25. class ThirdPartyCompanyBody(BaseModel):
  26. model_config = ConfigDict(populate_by_name=True)
  27. company_code: str = Field(..., alias="companyCode")
  28. company_name: str = Field(..., alias="companyName")
  29. contact_name: Optional[str] = Field(None, alias="contactName")
  30. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  31. address: Optional[str] = None
  32. invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
  33. tax_id: Optional[str] = Field(None, alias="taxId")
  34. bank_name: Optional[str] = Field(None, alias="bankName")
  35. bank_account: Optional[str] = Field(None, alias="bankAccount")
  36. status: str = "1"
  37. order_num: Optional[int] = Field(None, alias="orderNum")
  38. remark: Optional[str] = None
  39. class ThirdPartyCompanyUpdateBody(BaseModel):
  40. model_config = ConfigDict(populate_by_name=True)
  41. company_code: Optional[str] = Field(None, alias="companyCode")
  42. company_name: Optional[str] = Field(None, alias="companyName")
  43. contact_name: Optional[str] = Field(None, alias="contactName")
  44. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  45. address: Optional[str] = None
  46. invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
  47. tax_id: Optional[str] = Field(None, alias="taxId")
  48. bank_name: Optional[str] = Field(None, alias="bankName")
  49. bank_account: Optional[str] = Field(None, alias="bankAccount")
  50. status: Optional[str] = None
  51. order_num: Optional[int] = Field(None, alias="orderNum")
  52. remark: Optional[str] = None
  53. @router.get("/qc-third-party-companies")
  54. async def list_qc_third_party_companies(
  55. page: int = Query(1, ge=1),
  56. pageSize: int = Query(10, ge=1, le=100),
  57. keyword: Optional[str] = None,
  58. status: Optional[str] = None,
  59. db: Session = Depends(get_db),
  60. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  61. ):
  62. try:
  63. tenant_no = _get_tenant_no(db, user)
  64. items, total = tpc_svc.list_companies(
  65. db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
  66. user_info=user
  67. )
  68. return success_response(_page_payload(items, total, page, pageSize))
  69. except PermissionError as e:
  70. return error_response(403, str(e))
  71. except Exception as e:
  72. return error_response(400, str(e))
  73. @router.get("/qc-third-party-companies/options")
  74. async def list_qc_third_party_company_options(
  75. db: Session = Depends(get_db),
  76. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  77. ):
  78. try:
  79. tenant_no = _get_tenant_no(db, user)
  80. return success_response(tpc_svc.list_company_options(db, tenant_no))
  81. except PermissionError as e:
  82. return error_response(403, str(e))
  83. except Exception as e:
  84. return error_response(400, str(e))
  85. @router.post("/qc-third-party-companies")
  86. async def create_qc_third_party_company(
  87. body: ThirdPartyCompanyBody,
  88. db: Session = Depends(get_db),
  89. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  90. ):
  91. try:
  92. tenant_no = _get_tenant_no(db, user)
  93. obj = tpc_svc.create_company(
  94. db,
  95. company_code=body.company_code,
  96. company_name=body.company_name,
  97. contact_name=body.contact_name,
  98. contact_phone=body.contact_phone,
  99. address=body.address,
  100. invoice_title=body.invoice_title,
  101. tax_id=body.tax_id,
  102. bank_name=body.bank_name,
  103. bank_account=body.bank_account,
  104. status=body.status,
  105. tenant_no=tenant_no,
  106. order_num=body.order_num,
  107. remark=body.remark,
  108. user_id=_get_current_user_id(user),
  109. )
  110. return success_response(tpc_svc._row_to_dict(obj), message="创建成功")
  111. except PermissionError as e:
  112. return error_response(403, str(e))
  113. except ValueError as e:
  114. return error_response(400, str(e))
  115. except Exception as e:
  116. return error_response(400, str(e))
  117. @router.put("/qc-third-party-companies/{company_id}")
  118. async def update_qc_third_party_company(
  119. company_id: str,
  120. body: ThirdPartyCompanyUpdateBody,
  121. db: Session = Depends(get_db),
  122. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  123. ):
  124. try:
  125. tenant_no = _get_tenant_no(db, user)
  126. obj = tpc_svc.update_company(
  127. db,
  128. company_id,
  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. address=body.address,
  134. invoice_title=body.invoice_title,
  135. tax_id=body.tax_id,
  136. bank_name=body.bank_name,
  137. bank_account=body.bank_account,
  138. status=body.status,
  139. order_num=body.order_num,
  140. remark=body.remark,
  141. user_id=_get_current_user_id(user),
  142. tenant_no=tenant_no,
  143. )
  144. return success_response(tpc_svc._row_to_dict(obj), message="更新成功")
  145. except PermissionError as e:
  146. return error_response(403, str(e))
  147. except ValueError as e:
  148. return error_response(400, str(e))
  149. except Exception as e:
  150. return error_response(400, str(e))
  151. @router.delete("/qc-third-party-companies/{company_id}")
  152. async def delete_qc_third_party_company(
  153. company_id: str,
  154. db: Session = Depends(get_db),
  155. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  156. ):
  157. try:
  158. tenant_no = _get_tenant_no(db, user)
  159. tpc_svc.soft_delete_company(db, company_id, tenant_no=tenant_no)
  160. return success_response(None, message="删除成功")
  161. except PermissionError as e:
  162. return error_response(403, str(e))
  163. except ValueError as e:
  164. return error_response(400, str(e))
  165. except Exception as e:
  166. return error_response(400, str(e))
  167. # ---------- 第三方检测项目 ----------
  168. class ThirdPartyItemBody(BaseModel):
  169. model_config = ConfigDict(populate_by_name=True)
  170. company_id: str = Field(..., alias="companyId")
  171. item_code: str = Field(..., alias="itemCode")
  172. item_name: str = Field(..., alias="itemName")
  173. fee_amount: float = Field(..., alias="feeAmount")
  174. status: str = "1"
  175. order_num: Optional[int] = Field(None, alias="orderNum")
  176. remark: Optional[str] = None
  177. class ThirdPartyItemUpdateBody(BaseModel):
  178. model_config = ConfigDict(populate_by_name=True)
  179. company_id: Optional[str] = Field(None, alias="companyId")
  180. item_code: Optional[str] = Field(None, alias="itemCode")
  181. item_name: Optional[str] = Field(None, alias="itemName")
  182. fee_amount: Optional[float] = Field(None, alias="feeAmount")
  183. status: Optional[str] = None
  184. order_num: Optional[int] = Field(None, alias="orderNum")
  185. remark: Optional[str] = None
  186. @router.get("/qc-third-party-items")
  187. async def list_qc_third_party_items(
  188. page: int = Query(1, ge=1),
  189. pageSize: int = Query(10, ge=1, le=100),
  190. keyword: Optional[str] = None,
  191. status: Optional[str] = None,
  192. companyId: Optional[str] = None,
  193. db: Session = Depends(get_db),
  194. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  195. ):
  196. try:
  197. tenant_no = _get_tenant_no(db, user)
  198. items, total = tpi_svc.list_items(
  199. db,
  200. page=page,
  201. page_size=pageSize,
  202. keyword=keyword,
  203. status=status,
  204. company_id=companyId,
  205. tenant_no=tenant_no,
  206. user_info=user,
  207. )
  208. return success_response(_page_payload(items, total, page, pageSize))
  209. except PermissionError as e:
  210. return error_response(403, str(e))
  211. except Exception as e:
  212. return error_response(400, str(e))
  213. @router.get("/qc-third-party-items/active")
  214. async def list_qc_third_party_items_active(
  215. companyId: Optional[str] = None,
  216. db: Session = Depends(get_db),
  217. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  218. ):
  219. try:
  220. tenant_no = _get_tenant_no(db, user)
  221. return success_response(tpi_svc.list_active_items(db, tenant_no, company_id=companyId, user_info=user))
  222. except PermissionError as e:
  223. return error_response(403, str(e))
  224. except Exception as e:
  225. return error_response(400, str(e))
  226. @router.post("/qc-third-party-items")
  227. async def create_qc_third_party_item(
  228. body: ThirdPartyItemBody,
  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. obj = tpi_svc.create_item(
  235. db,
  236. company_id=body.company_id,
  237. item_code=body.item_code,
  238. item_name=body.item_name,
  239. fee_amount=body.fee_amount,
  240. status=body.status,
  241. tenant_no=tenant_no,
  242. order_num=body.order_num,
  243. remark=body.remark,
  244. user_id=_get_current_user_id(user),
  245. )
  246. return success_response(tpi_svc._row_to_dict(obj, ""), message="创建成功")
  247. except PermissionError as e:
  248. return error_response(403, str(e))
  249. except ValueError as e:
  250. return error_response(400, str(e))
  251. except Exception as e:
  252. return error_response(400, str(e))
  253. @router.put("/qc-third-party-items/{item_id}")
  254. async def update_qc_third_party_item(
  255. item_id: str,
  256. body: ThirdPartyItemUpdateBody,
  257. db: Session = Depends(get_db),
  258. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  259. ):
  260. try:
  261. tenant_no = _get_tenant_no(db, user)
  262. obj = tpi_svc.update_item(
  263. db,
  264. item_id,
  265. company_id=body.company_id,
  266. item_code=body.item_code,
  267. item_name=body.item_name,
  268. fee_amount=body.fee_amount,
  269. status=body.status,
  270. order_num=body.order_num,
  271. remark=body.remark,
  272. user_id=_get_current_user_id(user),
  273. tenant_no=tenant_no,
  274. )
  275. return success_response(tpi_svc._row_to_dict(obj, ""), message="更新成功")
  276. except PermissionError as e:
  277. return error_response(403, str(e))
  278. except ValueError as e:
  279. return error_response(400, str(e))
  280. except Exception as e:
  281. return error_response(400, str(e))
  282. @router.delete("/qc-third-party-items/{item_id}")
  283. async def delete_qc_third_party_item(
  284. item_id: str,
  285. db: Session = Depends(get_db),
  286. user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
  287. ):
  288. try:
  289. tenant_no = _get_tenant_no(db, user)
  290. tpi_svc.soft_delete_item(db, item_id, tenant_no=tenant_no)
  291. return success_response(None, message="删除成功")
  292. except PermissionError as e:
  293. return error_response(403, str(e))
  294. except ValueError as e:
  295. return error_response(400, str(e))
  296. except Exception as e:
  297. return error_response(400, str(e))
  298. # ---------- 实验室自检项目 ----------
  299. class LabTestItemBody(BaseModel):
  300. model_config = ConfigDict(populate_by_name=True)
  301. item_code: str = Field(..., alias="itemCode")
  302. item_name: str = Field(..., alias="itemName")
  303. description: Optional[str] = None
  304. status: str = "1"
  305. order_num: Optional[int] = Field(None, alias="orderNum")
  306. remark: Optional[str] = None
  307. class LabTestItemUpdateBody(BaseModel):
  308. model_config = ConfigDict(populate_by_name=True)
  309. item_code: Optional[str] = Field(None, alias="itemCode")
  310. item_name: Optional[str] = Field(None, alias="itemName")
  311. description: Optional[str] = None
  312. status: Optional[str] = None
  313. order_num: Optional[int] = Field(None, alias="orderNum")
  314. remark: Optional[str] = None
  315. # ---------- 检测项目参数模板 ----------
  316. class ItemParamBody(BaseModel):
  317. model_config = ConfigDict(populate_by_name=True)
  318. params: List[Dict[str, Any]] = Field(default_factory=list, alias="params")
  319. class TaskResultBody(BaseModel):
  320. model_config = ConfigDict(populate_by_name=True)
  321. results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
  322. class TaskCompleteWithResultBody(BaseModel):
  323. model_config = ConfigDict(populate_by_name=True)
  324. conclusion: str = Field(..., alias="conclusion")
  325. results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
  326. remark: Optional[str] = Field(None, alias="remark")
  327. third_party_attachments: Optional[str] = Field(None, alias="thirdPartyAttachments")
  328. material_usages: Optional[List[Dict[str, Any]]] = Field(None, alias="materialUsages")
  329. @router.get("/qc-lab-test-items")
  330. async def list_qc_lab_test_items(
  331. page: int = Query(1, ge=1),
  332. pageSize: int = Query(10, ge=1, le=100),
  333. keyword: Optional[str] = None,
  334. status: Optional[str] = None,
  335. db: Session = Depends(get_db),
  336. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  337. ):
  338. try:
  339. tenant_no = _get_tenant_no(db, user)
  340. items, total = lab_svc.list_lab_items(
  341. db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
  342. user_info=user
  343. )
  344. return success_response(_page_payload(items, total, page, pageSize))
  345. except PermissionError as e:
  346. return error_response(403, str(e))
  347. except Exception as e:
  348. return error_response(400, str(e))
  349. @router.get("/qc-lab-test-items/active")
  350. async def list_qc_lab_test_items_active(
  351. db: Session = Depends(get_db),
  352. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  353. ):
  354. try:
  355. tenant_no = _get_tenant_no(db, user)
  356. return success_response(lab_svc.list_active_lab_items(db, tenant_no, user_info=user))
  357. except PermissionError as e:
  358. return error_response(403, str(e))
  359. except Exception as e:
  360. return error_response(400, str(e))
  361. @router.post("/qc-lab-test-items")
  362. async def create_qc_lab_test_item(
  363. body: LabTestItemBody,
  364. db: Session = Depends(get_db),
  365. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  366. ):
  367. try:
  368. tenant_no = _get_tenant_no(db, user)
  369. obj = lab_svc.create_lab_item(
  370. db,
  371. item_code=body.item_code,
  372. item_name=body.item_name,
  373. description=body.description,
  374. status=body.status,
  375. tenant_no=tenant_no,
  376. order_num=body.order_num,
  377. remark=body.remark,
  378. user_id=_get_current_user_id(user),
  379. )
  380. return success_response(lab_svc._row_to_dict(obj), message="创建成功")
  381. except PermissionError as e:
  382. return error_response(403, str(e))
  383. except ValueError as e:
  384. return error_response(400, str(e))
  385. except Exception as e:
  386. return error_response(400, str(e))
  387. @router.put("/qc-lab-test-items/{item_id}")
  388. async def update_qc_lab_test_item(
  389. item_id: str,
  390. body: LabTestItemUpdateBody,
  391. db: Session = Depends(get_db),
  392. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  393. ):
  394. try:
  395. tenant_no = _get_tenant_no(db, user)
  396. obj = lab_svc.update_lab_item(
  397. db,
  398. item_id,
  399. item_code=body.item_code,
  400. item_name=body.item_name,
  401. description=body.description,
  402. status=body.status,
  403. order_num=body.order_num,
  404. remark=body.remark,
  405. user_id=_get_current_user_id(user),
  406. tenant_no=tenant_no,
  407. )
  408. return success_response(lab_svc._row_to_dict(obj), message="更新成功")
  409. except PermissionError as e:
  410. return error_response(403, str(e))
  411. except ValueError as e:
  412. return error_response(400, str(e))
  413. except Exception as e:
  414. return error_response(400, str(e))
  415. @router.delete("/qc-lab-test-items/{item_id}")
  416. async def delete_qc_lab_test_item(
  417. item_id: str,
  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. lab_svc.soft_delete_lab_item(db, item_id, tenant_no=tenant_no)
  424. return success_response(None, message="删除成功")
  425. except PermissionError as e:
  426. return error_response(403, str(e))
  427. except ValueError as e:
  428. return error_response(400, str(e))
  429. except Exception as e:
  430. return error_response(400, str(e))
  431. # ---------- 检测项目参数模板 ----------
  432. @router.get("/qc-item-params")
  433. async def list_item_params(
  434. itemSource: str = Query(...),
  435. itemId: str = Query(...),
  436. db: Session = Depends(get_db),
  437. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  438. ):
  439. try:
  440. tenant_no = _get_tenant_no(db, user)
  441. return success_response(param_svc.list_params_by_item(db, itemSource, itemId, tenant_no, user_info=user))
  442. except PermissionError as e:
  443. return error_response(403, str(e))
  444. except Exception as e:
  445. return error_response(400, str(e))
  446. @router.put("/qc-item-params")
  447. async def save_item_params(
  448. body: ItemParamBody,
  449. itemSource: str = Query(...),
  450. itemId: str = Query(...),
  451. db: Session = Depends(get_db),
  452. user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
  453. ):
  454. try:
  455. tenant_no = _get_tenant_no(db, user)
  456. return success_response(
  457. param_svc.save_params_for_item(db, itemSource, itemId, body.params, tenant_no, _get_current_user_id(user)),
  458. message="保存成功",
  459. )
  460. except PermissionError as e:
  461. return error_response(403, str(e))
  462. except Exception as e:
  463. return error_response(400, str(e))
  464. # ---------- 质检工单 ----------
  465. class WorkOrderConfigureBody(BaseModel):
  466. model_config = ConfigDict(populate_by_name=True)
  467. has_lab: bool = Field(False, alias="hasLab")
  468. has_third_party: bool = Field(False, alias="hasThirdParty")
  469. lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
  470. third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
  471. @router.get("/qc-work-orders")
  472. async def list_qc_work_orders(
  473. page: int = Query(1, ge=1),
  474. pageSize: int = Query(10, ge=1, le=100),
  475. keyword: Optional[str] = None,
  476. workOrderStatus: Optional[str] = None,
  477. db: Session = Depends(get_db),
  478. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  479. ):
  480. try:
  481. tenant_no = _get_tenant_no(db, user)
  482. items, total = wo_svc.list_work_orders(
  483. db,
  484. page=page,
  485. page_size=pageSize,
  486. keyword=keyword,
  487. work_order_status=workOrderStatus,
  488. tenant_no=tenant_no,
  489. user_info=user,
  490. )
  491. return success_response(_page_payload(items, total, page, pageSize))
  492. except PermissionError as e:
  493. return error_response(403, str(e))
  494. except Exception as e:
  495. return error_response(400, str(e))
  496. @router.get("/qc-work-orders/{work_order_id}")
  497. async def get_qc_work_order_detail(
  498. work_order_id: str,
  499. db: Session = Depends(get_db),
  500. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  501. ):
  502. try:
  503. tenant_no = _get_tenant_no(db, user)
  504. data = wo_svc.get_work_order_detail(db, work_order_id, tenant_no)
  505. return success_response(data)
  506. except PermissionError as e:
  507. return error_response(403, str(e))
  508. except ValueError as e:
  509. return error_response(400, str(e))
  510. except Exception as e:
  511. return error_response(400, str(e))
  512. @router.post("/qc-work-orders/{work_order_id}/configure")
  513. async def configure_qc_work_order(
  514. work_order_id: str,
  515. body: WorkOrderConfigureBody,
  516. db: Session = Depends(get_db),
  517. user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
  518. ):
  519. try:
  520. tenant_no = _get_tenant_no(db, user)
  521. obj = wo_svc.configure_work_order(
  522. db,
  523. work_order_id,
  524. has_lab=body.has_lab,
  525. has_third_party=body.has_third_party,
  526. lab_item_ids=body.lab_item_ids,
  527. third_party_item_ids=body.third_party_item_ids,
  528. tenant_no=tenant_no,
  529. user_id=_get_current_user_id(user),
  530. )
  531. return success_response(wo_svc._wo_to_dict(obj), message="配置成功")
  532. except PermissionError as e:
  533. return error_response(403, str(e))
  534. except ValueError as e:
  535. return error_response(400, str(e))
  536. except Exception as e:
  537. return error_response(400, str(e))
  538. # ---------- 质检任务 ----------
  539. class TaskAttachmentsBody(BaseModel):
  540. model_config = ConfigDict(populate_by_name=True)
  541. third_party_attachments: str = Field("", alias="thirdPartyAttachments")
  542. @router.get("/qc-tasks")
  543. async def list_qc_tasks(
  544. page: int = Query(1, ge=1),
  545. pageSize: int = Query(10, ge=1, le=100),
  546. keyword: Optional[str] = None,
  547. taskType: Optional[str] = None,
  548. taskStatus: Optional[str] = None,
  549. workOrderId: Optional[str] = None,
  550. qcBizType: Optional[str] = None,
  551. db: Session = Depends(get_db),
  552. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  553. ):
  554. try:
  555. tenant_no = _get_tenant_no(db, user)
  556. items, total = task_svc.list_tasks(
  557. db,
  558. page=page,
  559. page_size=pageSize,
  560. keyword=keyword,
  561. task_type=taskType,
  562. task_status=taskStatus,
  563. work_order_id=workOrderId,
  564. qc_biz_type=qcBizType,
  565. tenant_no=tenant_no,
  566. user_info=user,
  567. )
  568. return success_response(_page_payload(items, total, page, pageSize))
  569. except PermissionError as e:
  570. return error_response(403, str(e))
  571. except Exception as e:
  572. return error_response(400, str(e))
  573. @router.get("/qc-tasks/{task_id}")
  574. async def get_qc_task_detail(
  575. task_id: str,
  576. db: Session = Depends(get_db),
  577. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  578. ):
  579. try:
  580. tenant_no = _get_tenant_no(db, user)
  581. data = task_svc.get_task_detail(db, task_id, tenant_no)
  582. return success_response(data)
  583. except PermissionError as e:
  584. return error_response(403, str(e))
  585. except ValueError as e:
  586. return error_response(400, str(e))
  587. except Exception as e:
  588. return error_response(400, str(e))
  589. @router.post("/qc-tasks/{task_id}/start")
  590. async def start_qc_task(
  591. task_id: str,
  592. db: Session = Depends(get_db),
  593. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  594. ):
  595. try:
  596. tenant_no = _get_tenant_no(db, user)
  597. obj = task_svc.start_task(db, task_id, tenant_no=tenant_no, user_id=_get_current_user_id(user))
  598. return success_response(task_svc._task_to_dict(obj), message="已开始")
  599. except PermissionError as e:
  600. return error_response(403, str(e))
  601. except ValueError as e:
  602. return error_response(400, str(e))
  603. except Exception as e:
  604. return error_response(400, str(e))
  605. @router.post("/qc-tasks/{task_id}/complete")
  606. async def complete_qc_task(
  607. task_id: str,
  608. db: Session = Depends(get_db),
  609. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  610. ):
  611. try:
  612. tenant_no = _get_tenant_no(db, user)
  613. obj = task_svc.complete_task(
  614. db, task_id, tenant_no=tenant_no, user_id=_get_current_user_id(user)
  615. )
  616. return success_response(task_svc._task_to_dict(obj), message="已完成")
  617. except PermissionError as e:
  618. return error_response(403, str(e))
  619. except ValueError as e:
  620. return error_response(400, str(e))
  621. except Exception as e:
  622. return error_response(400, str(e))
  623. @router.get("/qc-tasks/{task_id}/results")
  624. async def get_task_results(
  625. task_id: str,
  626. db: Session = Depends(get_db),
  627. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  628. ):
  629. try:
  630. tenant_no = _get_tenant_no(db, user)
  631. return success_response(result_svc.list_results_by_task(db, task_id, tenant_no, user_info=user))
  632. except PermissionError as e:
  633. return error_response(403, str(e))
  634. except Exception as e:
  635. return error_response(400, str(e))
  636. @router.get("/qc-tasks/{task_id}/material-usages")
  637. async def get_task_material_usages(
  638. task_id: str,
  639. db: Session = Depends(get_db),
  640. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  641. ):
  642. try:
  643. tenant_no = _get_tenant_no(db, user)
  644. t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
  645. if not t:
  646. return error_response(404, "任务不存在")
  647. return success_response(
  648. mu_svc.list_usages_by_task(db, task_id, tenant_no, user_info=user)
  649. )
  650. except PermissionError as e:
  651. return error_response(403, str(e))
  652. except Exception as e:
  653. return error_response(400, str(e))
  654. @router.post("/qc-tasks/{task_id}/complete-with-result")
  655. async def complete_task_with_result(
  656. task_id: str,
  657. body: TaskCompleteWithResultBody,
  658. db: Session = Depends(get_db),
  659. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  660. ):
  661. try:
  662. tenant_no = _get_tenant_no(db, user)
  663. user_id = _get_current_user_id(user)
  664. t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
  665. if not t:
  666. return error_response(404, "任务不存在")
  667. was_done = (t.task_status or "").strip() == "ywc"
  668. if body.results:
  669. result_svc.save_results_for_task(
  670. db, task_id, body.results, tenant_no, user_id, commit=False,
  671. )
  672. concl = (body.conclusion or "").strip()
  673. if concl not in ("tg", "btg"):
  674. return error_response(400, "请选择检测结论(通过/不通过)")
  675. if body.third_party_attachments is not None:
  676. task_svc.update_task_attachments(
  677. db, task_id, attachments=body.third_party_attachments,
  678. tenant_no=tenant_no, user_id=user_id, commit=False,
  679. )
  680. obj = task_svc.apply_task_result(
  681. db,
  682. task_id,
  683. tenant_no=tenant_no,
  684. user_id=user_id,
  685. conclusion=concl,
  686. remark=body.remark,
  687. material_usages=body.material_usages,
  688. commit=False,
  689. )
  690. db.commit()
  691. db.refresh(obj)
  692. if (obj.task_status or "").strip() == "ywc":
  693. wo_svc.try_complete_work_order(db, obj.work_order_id, tenant_no)
  694. return success_response(
  695. task_svc._task_to_dict(obj, work_order_no="", db=db, tenant_no=tenant_no),
  696. message="已保存" if was_done else "已完成",
  697. )
  698. except HTTPException as e:
  699. db.rollback()
  700. detail = e.detail
  701. msg = detail if isinstance(detail, str) else str(detail)
  702. return error_response(e.status_code, msg)
  703. except PermissionError as e:
  704. db.rollback()
  705. return error_response(403, str(e))
  706. except ValueError as e:
  707. db.rollback()
  708. return error_response(400, str(e))
  709. except Exception as e:
  710. db.rollback()
  711. return error_response(400, str(e))
  712. @router.put("/qc-tasks/{task_id}/attachments")
  713. async def update_qc_task_attachments(
  714. task_id: str,
  715. body: TaskAttachmentsBody,
  716. db: Session = Depends(get_db),
  717. user: dict = Depends(require_menu_code("biz:qcTask:list")),
  718. ):
  719. try:
  720. tenant_no = _get_tenant_no(db, user)
  721. obj = task_svc.update_task_attachments(
  722. db,
  723. task_id,
  724. attachments=body.third_party_attachments,
  725. tenant_no=tenant_no,
  726. user_id=_get_current_user_id(user),
  727. )
  728. return success_response(task_svc._task_to_dict(obj), message="已保存")
  729. except PermissionError as e:
  730. return error_response(403, str(e))
  731. except ValueError as e:
  732. return error_response(400, str(e))
  733. except Exception as e:
  734. return error_response(400, str(e))
  735. # ---------- 质检物料库 ----------
  736. class QcMaterialBody(BaseModel):
  737. model_config = ConfigDict(populate_by_name=True)
  738. material_code: str = Field(..., alias="materialCode")
  739. material_name: str = Field(..., alias="materialName")
  740. spec_model: Optional[str] = Field(None, alias="specModel")
  741. unit: Optional[str] = None
  742. warning_qty: Optional[float] = Field(0, alias="warningQty")
  743. status: str = "1"
  744. remark: Optional[str] = None
  745. class QcMaterialUpdateBody(BaseModel):
  746. model_config = ConfigDict(populate_by_name=True)
  747. material_code: Optional[str] = Field(None, alias="materialCode")
  748. material_name: Optional[str] = Field(None, alias="materialName")
  749. spec_model: Optional[str] = Field(None, alias="specModel")
  750. unit: Optional[str] = None
  751. warning_qty: Optional[float] = Field(None, alias="warningQty")
  752. status: Optional[str] = None
  753. remark: Optional[str] = None
  754. class StockInBody(BaseModel):
  755. model_config = ConfigDict(populate_by_name=True)
  756. qty: float = Field(..., gt=0)
  757. remark: Optional[str] = None
  758. stock_in_time: Optional[str] = Field(None, alias="stockInTime")
  759. class StockOutBody(BaseModel):
  760. model_config = ConfigDict(populate_by_name=True)
  761. qty: float = Field(..., gt=0)
  762. receiver: Optional[str] = None
  763. purpose: Optional[str] = None
  764. remark: Optional[str] = None
  765. stock_out_time: Optional[str] = Field(None, alias="stockOutTime")
  766. class BatchStockInItem(BaseModel):
  767. model_config = ConfigDict(populate_by_name=True)
  768. material_id: str = Field(..., alias="materialId")
  769. qty: float = Field(..., gt=0)
  770. remark: Optional[str] = None
  771. class BatchStockInBody(BaseModel):
  772. model_config = ConfigDict(populate_by_name=True)
  773. items: List[BatchStockInItem] = Field(..., min_length=1)
  774. remark: Optional[str] = None
  775. stock_in_time: Optional[str] = Field(None, alias="stockInTime")
  776. class BatchStockOutItem(BaseModel):
  777. model_config = ConfigDict(populate_by_name=True)
  778. material_id: str = Field(..., alias="materialId")
  779. qty: float = Field(..., gt=0)
  780. remark: Optional[str] = None
  781. class BatchStockOutBody(BaseModel):
  782. model_config = ConfigDict(populate_by_name=True)
  783. items: List[BatchStockOutItem] = Field(..., min_length=1)
  784. receiver: Optional[str] = None
  785. purpose: Optional[str] = None
  786. remark: Optional[str] = None
  787. stock_out_time: Optional[str] = Field(None, alias="stockOutTime")
  788. @router.get("/qc-materials")
  789. async def list_qc_materials(
  790. page: int = Query(1, ge=1),
  791. pageSize: int = Query(10, ge=1, le=100),
  792. keyword: Optional[str] = None,
  793. status: Optional[str] = None,
  794. db: Session = Depends(get_db),
  795. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  796. ):
  797. try:
  798. tenant_no = _get_tenant_no(db, user)
  799. items, total = mat_svc.list_materials(
  800. db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
  801. user_info=user,
  802. )
  803. return success_response(_page_payload(items, total, page, pageSize))
  804. except PermissionError as e:
  805. return error_response(403, str(e))
  806. except Exception as e:
  807. return error_response(400, str(e))
  808. @router.get("/qc-materials/options")
  809. async def qc_material_options(
  810. db: Session = Depends(get_db),
  811. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  812. ):
  813. try:
  814. tenant_no = _get_tenant_no(db, user)
  815. return success_response(mat_svc.options_list(db, tenant_no=tenant_no))
  816. except Exception as e:
  817. return error_response(400, str(e))
  818. @router.post("/qc-materials")
  819. async def create_qc_material(
  820. body: QcMaterialBody,
  821. db: Session = Depends(get_db),
  822. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  823. ):
  824. try:
  825. tenant_no = _get_tenant_no(db, user)
  826. user_id = _get_current_user_id(user)
  827. obj = mat_svc.create_material(
  828. db,
  829. material_code=body.material_code,
  830. material_name=body.material_name,
  831. spec_model=body.spec_model,
  832. unit=body.unit,
  833. warning_qty=Decimal(str(body.warning_qty))
  834. if body.warning_qty is not None
  835. else None,
  836. status=mat_svc._norm_status(body.status),
  837. remark=body.remark,
  838. tenant_no=tenant_no,
  839. user_id=user_id,
  840. )
  841. return success_response(mat_svc._row_to_dict(obj), message="创建成功")
  842. except ValueError as e:
  843. return error_response(400, str(e))
  844. except Exception as e:
  845. return error_response(400, str(e))
  846. @router.put("/qc-materials/{material_id}")
  847. async def update_qc_material(
  848. material_id: str,
  849. body: QcMaterialUpdateBody,
  850. db: Session = Depends(get_db),
  851. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  852. ):
  853. try:
  854. tenant_no = _get_tenant_no(db, user)
  855. user_id = _get_current_user_id(user)
  856. data = body.model_dump(exclude_none=True)
  857. if "warning_qty" in data:
  858. data["warning_qty"] = Decimal(str(data["warning_qty"]))
  859. if "status" in data:
  860. data["status"] = mat_svc._norm_status(data["status"])
  861. obj = mat_svc.update_material(
  862. db,
  863. material_id,
  864. tenant_no=tenant_no,
  865. user_id=user_id,
  866. **data,
  867. )
  868. return success_response(mat_svc._row_to_dict(obj), message="更新成功")
  869. except ValueError as e:
  870. return error_response(400, str(e))
  871. except Exception as e:
  872. return error_response(400, str(e))
  873. @router.delete("/qc-materials/{material_id}")
  874. async def delete_qc_material(
  875. material_id: str,
  876. db: Session = Depends(get_db),
  877. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  878. ):
  879. try:
  880. tenant_no = _get_tenant_no(db, user)
  881. user_id = _get_current_user_id(user)
  882. mat_svc.soft_delete_material(db, material_id, tenant_no=tenant_no)
  883. return success_response(None, message="删除成功")
  884. except ValueError as e:
  885. return error_response(400, str(e))
  886. except Exception as e:
  887. return error_response(400, str(e))
  888. @router.post("/qc-materials/batch-stock-in")
  889. async def batch_stock_in_qc_material(
  890. body: BatchStockInBody,
  891. db: Session = Depends(get_db),
  892. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  893. ):
  894. try:
  895. tenant_no = _get_tenant_no(db, user)
  896. user_id = _get_current_user_id(user)
  897. operate_time = datetime.now()
  898. if body.stock_in_time:
  899. rt = parse_datetime(body.stock_in_time)
  900. if not rt:
  901. return error_response(400, "入库时间格式无效")
  902. operate_time = rt
  903. items = [
  904. {"material_id": x.material_id, "qty": x.qty, "remark": x.remark}
  905. for x in body.items
  906. ]
  907. count = mat_svc.batch_stock_in(
  908. db,
  909. items=items,
  910. remark=body.remark,
  911. tenant_no=tenant_no,
  912. user_id=user_id,
  913. operate_time=operate_time,
  914. )
  915. return success_response({"count": count}, message=f"批量入库成功,共 {count} 项")
  916. except ValueError as e:
  917. return error_response(400, str(e))
  918. except Exception as e:
  919. return error_response(400, str(e))
  920. @router.post("/qc-materials/batch-stock-out")
  921. async def batch_stock_out_qc_material(
  922. body: BatchStockOutBody,
  923. db: Session = Depends(get_db),
  924. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  925. ):
  926. try:
  927. tenant_no = _get_tenant_no(db, user)
  928. user_id = _get_current_user_id(user)
  929. operate_time = datetime.now()
  930. if body.stock_out_time:
  931. rt = parse_datetime(body.stock_out_time)
  932. if not rt:
  933. return error_response(400, "出库时间格式无效")
  934. operate_time = rt
  935. items = [
  936. {"material_id": x.material_id, "qty": x.qty, "remark": x.remark}
  937. for x in body.items
  938. ]
  939. count = mat_svc.batch_stock_out(
  940. db,
  941. items=items,
  942. receiver=body.receiver,
  943. purpose=body.purpose,
  944. remark=body.remark,
  945. tenant_no=tenant_no,
  946. user_id=user_id,
  947. operate_time=operate_time,
  948. )
  949. return success_response({"count": count}, message=f"批量出库成功,共 {count} 项")
  950. except ValueError as e:
  951. return error_response(400, str(e))
  952. except Exception as e:
  953. return error_response(400, str(e))
  954. @router.post("/qc-materials/{material_id}/stock-in")
  955. async def stock_in_qc_material(
  956. material_id: str,
  957. body: StockInBody,
  958. db: Session = Depends(get_db),
  959. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  960. ):
  961. try:
  962. tenant_no = _get_tenant_no(db, user)
  963. user_id = _get_current_user_id(user)
  964. operate_time = datetime.now()
  965. if body.stock_in_time:
  966. rt = parse_datetime(body.stock_in_time)
  967. if not rt:
  968. return error_response(400, "入库时间格式无效")
  969. operate_time = rt
  970. from decimal import Decimal
  971. obj = mat_svc.stock_in(
  972. db,
  973. material_id,
  974. qty=Decimal(str(body.qty)),
  975. remark=body.remark,
  976. tenant_no=tenant_no,
  977. user_id=user_id,
  978. operate_time=operate_time,
  979. )
  980. return success_response(mat_svc._row_to_dict(obj), message="入库成功")
  981. except ValueError as e:
  982. return error_response(400, str(e))
  983. except Exception as e:
  984. return error_response(400, str(e))
  985. @router.post("/qc-materials/{material_id}/stock-out")
  986. async def stock_out_qc_material(
  987. material_id: str,
  988. body: StockOutBody,
  989. db: Session = Depends(get_db),
  990. user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
  991. ):
  992. try:
  993. tenant_no = _get_tenant_no(db, user)
  994. user_id = _get_current_user_id(user)
  995. operate_time = datetime.now()
  996. if body.stock_out_time:
  997. rt = parse_datetime(body.stock_out_time)
  998. if not rt:
  999. return error_response(400, "出库时间格式无效")
  1000. operate_time = rt
  1001. from decimal import Decimal
  1002. obj = mat_svc.stock_out(
  1003. db,
  1004. material_id,
  1005. qty=Decimal(str(body.qty)),
  1006. receiver=body.receiver,
  1007. purpose=body.purpose,
  1008. remark=body.remark,
  1009. tenant_no=tenant_no,
  1010. user_id=user_id,
  1011. operate_time=operate_time,
  1012. )
  1013. return success_response(mat_svc._row_to_dict(obj), message="出库成功")
  1014. except ValueError as e:
  1015. return error_response(400, str(e))
  1016. except Exception as e:
  1017. return error_response(400, str(e))
  1018. # ---------- 物料变动记录 ----------
  1019. @router.get("/qc-material-usages")
  1020. async def list_qc_material_usages(
  1021. page: int = Query(1, ge=1),
  1022. pageSize: int = Query(10, ge=1, le=100),
  1023. materialId: Optional[str] = None,
  1024. changeType: Optional[str] = None,
  1025. startTime: Optional[str] = None,
  1026. endTime: Optional[str] = None,
  1027. db: Session = Depends(get_db),
  1028. user: dict = Depends(require_menu_code("biz:qcMaterialUsage:list")),
  1029. ):
  1030. try:
  1031. tenant_no = _get_tenant_no(db, user)
  1032. items, total = mu_svc.list_usages(
  1033. db, tenant_no, page=page, page_size=pageSize,
  1034. material_id=materialId, change_type=changeType,
  1035. start_time=startTime, end_time=endTime,
  1036. user_info=user,
  1037. )
  1038. return success_response(_page_payload(items, total, page, pageSize))
  1039. except PermissionError as e:
  1040. return error_response(403, str(e))
  1041. except Exception as e:
  1042. return error_response(400, str(e))