Wire create/update API, list keyword search, and response dict field. Co-authored-by: Cursor <cursoragent@cursor.com>
2720 lines
96 KiB
Python
2720 lines
96 KiB
Python
"""质量检测 — 第三方/实验室主数据、质检工单、质检任务 API"""
|
|
from typing import Any, Dict, Iterable, List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlalchemy.orm import Session
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
|
|
from app.core.database import get_db
|
|
from app.core.menu_guard import (
|
|
require_admin_or_super_admin,
|
|
require_any_menu_code,
|
|
require_menu_code,
|
|
strip_row_fields,
|
|
user_has_menu_perm,
|
|
)
|
|
|
|
MY_QC_MATERIAL_USAGE_VIEW_AMOUNT_PERM = "biz:myQcMaterialUsage:viewAmount"
|
|
QC_MATERIAL_VIEW_AMOUNT_PERM = "biz:qcMaterial:viewAmount"
|
|
QC_THIRD_PARTY_VIEW_AMOUNT_PERM = "biz:qcThirdPartyCompany:viewAmount"
|
|
QC_MATERIAL_AMOUNT_FIELD_NAMES = ("unitPrice", "amount", "remainingAmount", "unit_price")
|
|
QC_THIRD_PARTY_FEE_FIELD_NAMES = ("feeAmount", "thirdPartyFeeTotal")
|
|
from app.core.response import error_response, success_response
|
|
from app.core.utils import parse_datetime
|
|
from app.crud import qc_lab_test_item as lab_svc
|
|
from app.crud import qc_inspection_item_template as iit_svc
|
|
from app.crud import qc_item_param as param_svc
|
|
from app.crud import qc_task_result as result_svc
|
|
from app.crud import qc_task as task_svc
|
|
from app.crud import qc_third_party_company as tpc_svc
|
|
from app.crud import qc_third_party_item as tpi_svc
|
|
from app.crud import qc_material as mat_svc
|
|
from app.crud import qc_material_stock_batch as batch_svc
|
|
from app.crud import qc_material_stock_check as stock_check_svc
|
|
from app.crud import qc_material_usage as mu_svc
|
|
from app.crud import qc_material_amount_auth as amount_auth_svc
|
|
from app.crud import qc_material_month_template as month_tpl_svc
|
|
from app.crud.qc_material_stock_batch import _parse_date
|
|
from app.core.qc_material_amount_auth_constants import QC_MATERIAL_AMOUNT_AUTH_LIST_PERM
|
|
from app.crud import inbound_sample_special as inbound_sample_special_svc
|
|
from app.crud import qc_work_order as wo_svc
|
|
from app.crud.collection_protocol import total_pages
|
|
from app.api.sample_reception import _get_current_user_id, _get_tenant_no, _page_payload
|
|
from app.schemas.qc_work_order import QcWorkOrderCreateBody, QcWorkOrderUpdateBody
|
|
from app.schemas.qc_task import QcTaskCreateBody
|
|
|
|
|
|
router = APIRouter(prefix="/web/admin", tags=["质量检测"])
|
|
|
|
# 协议/捐赠查看等场景只读打开质检任务详情(参数、附件)
|
|
QC_TASK_READONLY_MENU_CODES = (
|
|
"biz:qcTask:list",
|
|
"biz:myQcTask:list",
|
|
"biz:cellStorageProtocol:list",
|
|
"biz:cellStorageProtocolAudit:list",
|
|
"biz:researchDonation:list",
|
|
"biz:researchDonationAudit:list",
|
|
"biz:storageReleaseReview:list",
|
|
"biz:qcWorkOrder:list",
|
|
)
|
|
|
|
|
|
# ---------- 第三方检测公司 ----------
|
|
|
|
|
|
class ThirdPartyCompanyBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
company_code: str = Field(..., alias="companyCode")
|
|
company_name: str = Field(..., alias="companyName")
|
|
contact_name: Optional[str] = Field(None, alias="contactName")
|
|
contact_phone: Optional[str] = Field(None, alias="contactPhone")
|
|
company_logo: Optional[str] = Field(None, alias="companyLogo")
|
|
address: Optional[str] = None
|
|
invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
|
|
tax_id: Optional[str] = Field(None, alias="taxId")
|
|
bank_name: Optional[str] = Field(None, alias="bankName")
|
|
bank_account: Optional[str] = Field(None, alias="bankAccount")
|
|
status: str = "1"
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class ThirdPartyCompanyUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
company_code: Optional[str] = Field(None, alias="companyCode")
|
|
company_name: Optional[str] = Field(None, alias="companyName")
|
|
contact_name: Optional[str] = Field(None, alias="contactName")
|
|
contact_phone: Optional[str] = Field(None, alias="contactPhone")
|
|
company_logo: Optional[str] = Field(None, alias="companyLogo")
|
|
address: Optional[str] = None
|
|
invoice_title: Optional[str] = Field(None, alias="invoiceTitle")
|
|
tax_id: Optional[str] = Field(None, alias="taxId")
|
|
bank_name: Optional[str] = Field(None, alias="bankName")
|
|
bank_account: Optional[str] = Field(None, alias="bankAccount")
|
|
status: Optional[str] = None
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/qc-third-party-companies")
|
|
async def list_qc_third_party_companies(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = tpc_svc.list_companies(
|
|
db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
|
|
user_info=user
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-third-party-companies/options")
|
|
async def list_qc_third_party_company_options(
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(tpc_svc.list_company_options(db, tenant_no))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-third-party-companies")
|
|
async def create_qc_third_party_company(
|
|
body: ThirdPartyCompanyBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = tpc_svc.create_company(
|
|
db,
|
|
company_code=body.company_code,
|
|
company_name=body.company_name,
|
|
contact_name=body.contact_name,
|
|
contact_phone=body.contact_phone,
|
|
company_logo=body.company_logo,
|
|
address=body.address,
|
|
invoice_title=body.invoice_title,
|
|
tax_id=body.tax_id,
|
|
bank_name=body.bank_name,
|
|
bank_account=body.bank_account,
|
|
status=body.status,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
)
|
|
return success_response(tpc_svc._row_to_dict(obj), message="创建成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-third-party-companies/{company_id}")
|
|
async def update_qc_third_party_company(
|
|
company_id: str,
|
|
body: ThirdPartyCompanyUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = tpc_svc.update_company(
|
|
db,
|
|
company_id,
|
|
company_code=body.company_code,
|
|
company_name=body.company_name,
|
|
contact_name=body.contact_name,
|
|
contact_phone=body.contact_phone,
|
|
company_logo=body.company_logo,
|
|
address=body.address,
|
|
invoice_title=body.invoice_title,
|
|
tax_id=body.tax_id,
|
|
bank_name=body.bank_name,
|
|
bank_account=body.bank_account,
|
|
status=body.status,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(tpc_svc._row_to_dict(obj), message="更新成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-third-party-companies/{company_id}")
|
|
async def delete_qc_third_party_company(
|
|
company_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
tpc_svc.soft_delete_company(db, company_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 第三方检测项目 ----------
|
|
|
|
|
|
class ThirdPartyItemBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
company_id: str = Field(..., alias="companyId")
|
|
item_code: str = Field(..., alias="itemCode")
|
|
item_name: str = Field(..., alias="itemName")
|
|
fee_amount: float = Field(..., alias="feeAmount")
|
|
status: str = "1"
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class ThirdPartyItemUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
company_id: Optional[str] = Field(None, alias="companyId")
|
|
item_code: Optional[str] = Field(None, alias="itemCode")
|
|
item_name: Optional[str] = Field(None, alias="itemName")
|
|
fee_amount: Optional[float] = Field(None, alias="feeAmount")
|
|
status: Optional[str] = None
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/qc-third-party-items")
|
|
async def list_qc_third_party_items(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
companyId: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = tpi_svc.list_items(
|
|
db,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
status=status,
|
|
company_id=companyId,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-third-party-items/active")
|
|
async def list_qc_third_party_items_active(
|
|
companyId: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(tpi_svc.list_active_items(db, tenant_no, company_id=companyId, user_info=user))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-third-party-items")
|
|
async def create_qc_third_party_item(
|
|
body: ThirdPartyItemBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = tpi_svc.create_item(
|
|
db,
|
|
company_id=body.company_id,
|
|
item_code=body.item_code,
|
|
item_name=body.item_name,
|
|
fee_amount=body.fee_amount,
|
|
status=body.status,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
)
|
|
return success_response(tpi_svc._row_to_dict(obj, ""), message="创建成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-third-party-items/{item_id}")
|
|
async def update_qc_third_party_item(
|
|
item_id: str,
|
|
body: ThirdPartyItemUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = tpi_svc.update_item(
|
|
db,
|
|
item_id,
|
|
company_id=body.company_id,
|
|
item_code=body.item_code,
|
|
item_name=body.item_name,
|
|
fee_amount=body.fee_amount,
|
|
status=body.status,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(tpi_svc._row_to_dict(obj, ""), message="更新成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-third-party-items/{item_id}")
|
|
async def delete_qc_third_party_item(
|
|
item_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcThirdPartyCompany:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
tpi_svc.soft_delete_item(db, item_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 实验室自检项目 ----------
|
|
|
|
|
|
class LabTestItemBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
item_code: str = Field(..., alias="itemCode")
|
|
item_name: str = Field(..., alias="itemName")
|
|
description: Optional[str] = None
|
|
status: str = "1"
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class LabTestItemUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
item_code: Optional[str] = Field(None, alias="itemCode")
|
|
item_name: Optional[str] = Field(None, alias="itemName")
|
|
description: Optional[str] = None
|
|
status: Optional[str] = None
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
# ---------- 检测项目模版(项目组合) ----------
|
|
|
|
|
|
class InspectionItemTemplateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
template_name: str = Field(..., alias="templateName")
|
|
lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
|
|
third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
|
|
status: Optional[str] = "1"
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class InspectionItemTemplateUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
template_name: Optional[str] = Field(None, alias="templateName")
|
|
lab_item_ids: Optional[List[str]] = Field(None, alias="labItemIds")
|
|
third_party_item_ids: Optional[List[str]] = Field(None, alias="thirdPartyItemIds")
|
|
status: Optional[str] = None
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
# ---------- 检测项目参数模板 ----------
|
|
|
|
|
|
class ItemParamBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
params: List[Dict[str, Any]] = Field(default_factory=list, alias="params")
|
|
|
|
|
|
class TaskResultBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
|
|
|
|
|
|
class TaskCompleteWithResultBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
conclusion: Optional[str] = Field(None, alias="conclusion")
|
|
results: List[Dict[str, Any]] = Field(default_factory=list, alias="results")
|
|
remark: Optional[str] = Field(None, alias="remark")
|
|
third_party_attachments: Optional[str] = Field(None, alias="thirdPartyAttachments")
|
|
material_usages: Optional[List[Dict[str, Any]]] = Field(None, alias="materialUsages")
|
|
task_status: Optional[str] = Field(None, alias="taskStatus")
|
|
|
|
|
|
@router.get("/qc-lab-test-items")
|
|
async def list_qc_lab_test_items(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = lab_svc.list_lab_items(
|
|
db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
|
|
user_info=user
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-lab-test-items/active")
|
|
async def list_qc_lab_test_items_active(
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(lab_svc.list_active_lab_items(db, tenant_no, user_info=user))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-lab-test-items")
|
|
async def create_qc_lab_test_item(
|
|
body: LabTestItemBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = lab_svc.create_lab_item(
|
|
db,
|
|
item_code=body.item_code,
|
|
item_name=body.item_name,
|
|
description=body.description,
|
|
status=body.status,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
)
|
|
return success_response(lab_svc._row_to_dict(obj), message="创建成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-lab-test-items/{item_id}")
|
|
async def update_qc_lab_test_item(
|
|
item_id: str,
|
|
body: LabTestItemUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = lab_svc.update_lab_item(
|
|
db,
|
|
item_id,
|
|
item_code=body.item_code,
|
|
item_name=body.item_name,
|
|
description=body.description,
|
|
status=body.status,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(lab_svc._row_to_dict(obj), message="更新成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-lab-test-items/{item_id}")
|
|
async def delete_qc_lab_test_item(
|
|
item_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
lab_svc.soft_delete_lab_item(db, item_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-inspection-item-templates")
|
|
async def list_qc_inspection_item_templates(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = iit_svc.list_templates(
|
|
db,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
status=status,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-inspection-item-templates/active")
|
|
async def list_qc_inspection_item_templates_active(
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(
|
|
require_any_menu_code(
|
|
"biz:qcInspectionItemTemplate:list", "biz:qcWorkOrder:list"
|
|
)
|
|
),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(
|
|
iit_svc.list_active_templates(db, tenant_no, user_info=user)
|
|
)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-inspection-item-templates/{template_id}")
|
|
async def get_qc_inspection_item_template(
|
|
template_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = iit_svc.get_template(db, template_id, tenant_no=tenant_no)
|
|
if not obj:
|
|
return error_response(404, "模版不存在")
|
|
return success_response(iit_svc._row_to_dict(obj))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-inspection-item-templates")
|
|
async def create_qc_inspection_item_template(
|
|
body: InspectionItemTemplateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:add")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = iit_svc.create_template(
|
|
db,
|
|
template_name=body.template_name,
|
|
lab_item_ids=body.lab_item_ids,
|
|
third_party_item_ids=body.third_party_item_ids,
|
|
status=body.status or "1",
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
create_dept_id=user.get("dept_id", "") or "",
|
|
)
|
|
return success_response(iit_svc._row_to_dict(obj), message="创建成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-inspection-item-templates/{template_id}")
|
|
async def update_qc_inspection_item_template(
|
|
template_id: str,
|
|
body: InspectionItemTemplateUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:edit")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = iit_svc.update_template(
|
|
db,
|
|
template_id,
|
|
template_name=body.template_name,
|
|
lab_item_ids=body.lab_item_ids,
|
|
third_party_item_ids=body.third_party_item_ids,
|
|
status=body.status,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=_get_current_user_id(user),
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(iit_svc._row_to_dict(obj), message="更新成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-inspection-item-templates/{template_id}")
|
|
async def delete_qc_inspection_item_template(
|
|
template_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcInspectionItemTemplate:remove")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
iit_svc.soft_delete_template(db, template_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 检测项目参数模板 ----------
|
|
|
|
|
|
@router.get("/qc-item-params")
|
|
async def list_item_params(
|
|
itemSource: str = Query(...),
|
|
itemId: str = Query(...),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(param_svc.list_params_by_item(db, itemSource, itemId, tenant_no, user_info=user))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-item-params")
|
|
async def save_item_params(
|
|
body: ItemParamBody,
|
|
itemSource: str = Query(...),
|
|
itemId: str = Query(...),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcLabTestItem:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(
|
|
param_svc.save_params_for_item(db, itemSource, itemId, body.params, tenant_no, _get_current_user_id(user)),
|
|
message="保存成功",
|
|
)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 质检工单 ----------
|
|
|
|
|
|
class WorkOrderConfigureBatchItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
id: Optional[str] = None
|
|
batch_name: Optional[str] = Field(None, alias="batchName")
|
|
assignee_user_ids: List[str] = Field(default_factory=list, alias="assigneeUserIds")
|
|
has_lab: bool = Field(False, alias="hasLab")
|
|
has_third_party: bool = Field(False, alias="hasThirdParty")
|
|
lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
|
|
third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
|
|
|
|
|
|
class WorkOrderConfigureBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
batches: Optional[List[WorkOrderConfigureBatchItem]] = Field(None, alias="batches")
|
|
has_lab: bool = Field(False, alias="hasLab")
|
|
has_third_party: bool = Field(False, alias="hasThirdParty")
|
|
lab_item_ids: List[str] = Field(default_factory=list, alias="labItemIds")
|
|
third_party_item_ids: List[str] = Field(default_factory=list, alias="thirdPartyItemIds")
|
|
lab_item_assignees: Dict[str, List[str]] = Field(default_factory=dict, alias="labItemAssignees")
|
|
third_party_item_assignees: Dict[str, List[str]] = Field(
|
|
default_factory=dict, alias="thirdPartyItemAssignees"
|
|
)
|
|
|
|
|
|
@router.post("/qc-work-orders")
|
|
async def create_qc_work_order(
|
|
body: QcWorkOrderCreateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
wo_type = (body.work_order_type or "inbound").strip()
|
|
if wo_type == "temporary":
|
|
wo = wo_svc.create_temp_qc_work_order(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
user_id=user.get("user_id"),
|
|
create_dept_id=user.get("dept_id", ""),
|
|
cell_product_id=body.cell_product_id or "",
|
|
external_target_code=body.external_target_code or "",
|
|
remark=body.remark or "",
|
|
)
|
|
else:
|
|
wo = wo_svc.create_qc_work_order_manual(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
user_id=user.get("user_id"),
|
|
create_dept_id=user.get("dept_id", ""),
|
|
protocol_no=body.protocol_no or "",
|
|
sample_code=body.sample_code or "",
|
|
reception_source=body.reception_source or "cell_storage",
|
|
qc_biz_type=body.qc_biz_type or "inbound",
|
|
remark=body.remark or "",
|
|
)
|
|
db.commit()
|
|
return success_response(wo_svc._wo_to_dict(wo), message="创建成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-work-orders")
|
|
async def list_qc_work_orders(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
workOrderStatus: Optional[str] = Query(None, description="工单状态,多个用英文逗号分隔"),
|
|
workOrderType: Optional[str] = None,
|
|
bizType: Optional[str] = Query(None, alias="bizType"),
|
|
receptionSource: Optional[str] = Query(None, alias="receptionSource"),
|
|
createTimeStart: Optional[str] = Query(None, alias="createTimeStart"),
|
|
createTimeEnd: Optional[str] = Query(None, alias="createTimeEnd"),
|
|
infusionTimeStart: Optional[str] = Query(None, alias="infusionTimeStart"),
|
|
infusionTimeEnd: Optional[str] = Query(None, alias="infusionTimeEnd"),
|
|
triggerSource: Optional[str] = Query(None, alias="triggerSource"),
|
|
sampleTypeId: Optional[str] = Query(None, alias="sampleTypeId"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = wo_svc.list_work_orders(
|
|
db,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
work_order_status=workOrderStatus,
|
|
work_order_type=workOrderType,
|
|
biz_type=bizType,
|
|
reception_source=receptionSource,
|
|
trigger_source=triggerSource,
|
|
sample_type_id=sampleTypeId,
|
|
create_time_start=createTimeStart,
|
|
create_time_end=createTimeEnd,
|
|
infusion_time_start=infusionTimeStart,
|
|
infusion_time_end=infusionTimeEnd,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-work-orders/tab-unfinished-counts")
|
|
async def qc_work_order_tab_unfinished_counts(
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
counts = wo_svc.count_unfinished_work_orders_by_tab(
|
|
db, tenant_no=tenant_no, user_info=user
|
|
)
|
|
return success_response(counts)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-work-orders/mine-tab-unfinished-counts")
|
|
async def qc_work_order_mine_tab_unfinished_counts(
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:myQcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
counts = wo_svc.count_mine_unfinished_work_orders_by_tab(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
assignee_user_id=_get_current_user_id(user),
|
|
)
|
|
return success_response(counts)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-work-orders/by-sample")
|
|
async def list_qc_work_orders_by_sample(
|
|
sampleCode: str = Query(..., alias="sampleCode"),
|
|
receptionSource: str = Query(..., alias="receptionSource"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(
|
|
require_any_menu_code("biz:qcWorkOrder:list", "biz:myQcTask:list")
|
|
),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = wo_svc.list_work_orders_by_sample(
|
|
db,
|
|
sample_code=sampleCode,
|
|
reception_source=receptionSource,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response({"list": items})
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-work-orders/{work_order_id}")
|
|
async def get_qc_work_order_detail(
|
|
work_order_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
data = wo_svc.get_work_order_detail(db, work_order_id, tenant_no)
|
|
_maybe_strip_qc_third_party_fee_fields(db, user, data)
|
|
return success_response(data)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-work-orders/{work_order_id}/outbound-detail")
|
|
async def get_outbound_qc_detail(
|
|
work_order_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
data = wo_svc.get_outbound_qc_detail(db, work_order_id, tenant_no)
|
|
_maybe_strip_qc_third_party_fee_fields(db, user, data)
|
|
return success_response(data)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-work-orders/{work_order_id}")
|
|
async def update_qc_work_order(
|
|
work_order_id: str,
|
|
body: QcWorkOrderUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
wo = wo_svc.update_qc_work_order(
|
|
db,
|
|
work_order_id,
|
|
tenant_no=tenant_no,
|
|
user_id=_get_current_user_id(user),
|
|
cell_product_id=body.cell_product_id,
|
|
external_target_code=body.external_target_code,
|
|
work_order_status=body.work_order_status,
|
|
qc_conclusion=body.qc_conclusion,
|
|
remark=body.remark,
|
|
)
|
|
db.commit()
|
|
return success_response(wo_svc._wo_to_dict(wo), message="保存成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-work-orders/{work_order_id}")
|
|
async def delete_qc_work_order_api(
|
|
work_order_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
wo_svc.delete_qc_work_order(db, work_order_id, tenant_no, user.get("user_id"))
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
class InboundSampleSpecialMarkBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
reception_source: str = Field(..., alias="receptionSource")
|
|
protocol_sample_id: str = Field(..., alias="protocolSampleId")
|
|
is_special: bool = Field(..., alias="isSpecial")
|
|
special_remark: Optional[str] = Field(None, alias="specialRemark")
|
|
|
|
|
|
@router.put("/inbound-samples/special-mark")
|
|
async def update_inbound_sample_special_mark(
|
|
body: InboundSampleSpecialMarkBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(
|
|
require_any_menu_code("biz:qcWorkOrder:list", "biz:myQcTask:list")
|
|
),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
source = (body.reception_source or "").strip()
|
|
if source not in ("cell_storage", "research_donation"):
|
|
return error_response(400, "仅支持细胞存储或科研捐赠样本")
|
|
data = inbound_sample_special_svc.update_inbound_sample_special_mark(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
reception_source=source,
|
|
protocol_sample_id=body.protocol_sample_id,
|
|
is_special=body.is_special,
|
|
special_remark=body.special_remark,
|
|
user_id=user.get("user_id") or "",
|
|
)
|
|
db.commit()
|
|
return success_response(data, message="保存成功")
|
|
except ValueError as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-work-orders/{work_order_id}/configure")
|
|
async def configure_qc_work_order(
|
|
work_order_id: str,
|
|
body: WorkOrderConfigureBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcWorkOrder:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = wo_svc.configure_work_order(
|
|
db,
|
|
work_order_id,
|
|
has_lab=body.has_lab,
|
|
has_third_party=body.has_third_party,
|
|
lab_item_ids=body.lab_item_ids,
|
|
third_party_item_ids=body.third_party_item_ids,
|
|
lab_item_assignees=body.lab_item_assignees,
|
|
third_party_item_assignees=body.third_party_item_assignees,
|
|
batches=[b.model_dump(by_alias=True) for b in body.batches] if body.batches is not None else None,
|
|
tenant_no=tenant_no,
|
|
user_id=_get_current_user_id(user),
|
|
create_dept_id=user.get("dept_id", ""),
|
|
)
|
|
return success_response(wo_svc._wo_to_dict(obj), message="配置成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
class WorkOrderInspectionBatchesBatchBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
work_order_ids: List[str] = Field(default_factory=list, alias="workOrderIds")
|
|
|
|
|
|
@router.post("/qc-work-orders/inspection-batches/batch")
|
|
async def batch_list_qc_work_order_inspection_batches(
|
|
body: WorkOrderInspectionBatchesBatchBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
from app.crud import qc_inspection_batch as batch_svc
|
|
|
|
data = batch_svc.list_inspection_batches_by_work_order_ids(
|
|
db, body.work_order_ids, tenant_no
|
|
)
|
|
return success_response(data)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-work-orders/{work_order_id}/inspection-batches")
|
|
async def list_qc_work_order_inspection_batches(
|
|
work_order_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
from app.crud import qc_inspection_batch as batch_svc
|
|
|
|
wo = wo_svc.get_work_order(db, work_order_id, tenant_no=tenant_no)
|
|
if not wo:
|
|
return error_response(404, "工单不存在")
|
|
return success_response(batch_svc.list_inspection_batches(db, work_order_id, tenant_no))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 质检任务 ----------
|
|
|
|
|
|
class TaskAttachmentsBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
third_party_attachments: str = Field("", alias="thirdPartyAttachments")
|
|
|
|
|
|
@router.post("/qc-tasks")
|
|
async def create_qc_task(
|
|
body: QcTaskCreateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
task = task_svc.create_qc_task_manual(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
user_id=user.get("user_id"),
|
|
create_dept_id=user.get("dept_id", ""),
|
|
task_type=body.task_type,
|
|
item_code=body.item_code,
|
|
item_name=body.item_name,
|
|
company_id=body.company_id,
|
|
company_name=body.company_name,
|
|
fee_amount=body.fee_amount,
|
|
sample_code=body.sample_code or "",
|
|
work_order_id=body.work_order_id or "",
|
|
remark=body.remark or "",
|
|
)
|
|
db.commit()
|
|
return success_response(task_svc._task_to_dict(task), message="创建成功")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-tasks/mine")
|
|
async def list_my_qc_tasks(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
taskType: Optional[str] = None,
|
|
taskStatus: Optional[str] = None,
|
|
qcBizType: Optional[str] = None,
|
|
receptionSource: Optional[str] = Query(None, alias="receptionSource"),
|
|
bizType: Optional[str] = Query(None, alias="bizType"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:myQcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = task_svc.list_mine_tasks(
|
|
db,
|
|
assignee_user_id=_get_current_user_id(user),
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
task_type=taskType,
|
|
task_status=taskStatus,
|
|
qc_biz_type=qcBizType,
|
|
reception_source=receptionSource,
|
|
biz_type=bizType,
|
|
tenant_no=tenant_no,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-tasks")
|
|
async def list_qc_tasks(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
taskType: Optional[str] = None,
|
|
taskStatus: Optional[str] = None,
|
|
workOrderId: Optional[str] = None,
|
|
qcBizType: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = task_svc.list_tasks(
|
|
db,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
task_type=taskType,
|
|
task_status=taskStatus,
|
|
work_order_id=workOrderId,
|
|
qc_biz_type=qcBizType,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-tasks/{task_id}")
|
|
async def get_qc_task_detail(
|
|
task_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
data = task_svc.get_task_detail(db, task_id, tenant_no)
|
|
return success_response(data)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-tasks/{task_id}/start")
|
|
async def start_qc_task(
|
|
task_id: str,
|
|
operateScope: Optional[str] = Query(None, alias="operateScope"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = task_svc.start_task(db, task_id, tenant_no=tenant_no, user_id=_get_current_user_id(user))
|
|
return success_response(task_svc._task_to_dict(obj), message="已开始")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-tasks/{task_id}/complete")
|
|
async def complete_qc_task(
|
|
task_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = task_svc.complete_task(
|
|
db, task_id, tenant_no=tenant_no, user_id=_get_current_user_id(user)
|
|
)
|
|
return success_response(task_svc._task_to_dict(obj), message="已完成")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-tasks/{task_id}/results")
|
|
async def get_task_results(
|
|
task_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(result_svc.list_results_by_task(db, task_id, tenant_no, user_info=user))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-material-requisition-options")
|
|
async def list_qc_material_requisition_options(
|
|
keyword: Optional[str] = None,
|
|
limit: int = Query(50, ge=1, le=100),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = mu_svc.list_requisition_options(
|
|
db,
|
|
tenant_no,
|
|
keyword=keyword,
|
|
limit=limit,
|
|
user_info=user,
|
|
)
|
|
return success_response(items)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-tasks/{task_id}/material-usages")
|
|
async def get_task_material_usages(
|
|
task_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code(*QC_TASK_READONLY_MENU_CODES)),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
|
|
if not t:
|
|
return error_response(404, "任务不存在")
|
|
return success_response(
|
|
mu_svc.list_usages_by_task(db, task_id, tenant_no, user_info=user)
|
|
)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-tasks/{task_id}/complete-with-result")
|
|
async def complete_task_with_result(
|
|
task_id: str,
|
|
body: TaskCompleteWithResultBody,
|
|
operateScope: Optional[str] = Query(None, alias="operateScope"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
t = task_svc.get_task(db, task_id, tenant_no=tenant_no)
|
|
if not t:
|
|
return error_response(404, "任务不存在")
|
|
was_done = (t.task_status or "").strip() == "ywc"
|
|
if body.results:
|
|
result_svc.save_results_for_task(
|
|
db, task_id, body.results, tenant_no, user_id, commit=False,
|
|
)
|
|
if body.third_party_attachments is not None:
|
|
task_svc.update_task_attachments(
|
|
db, task_id, attachments=body.third_party_attachments,
|
|
tenant_no=tenant_no, user_id=user_id, commit=False,
|
|
)
|
|
obj = task_svc.apply_task_result(
|
|
db,
|
|
task_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
conclusion=body.conclusion,
|
|
remark=body.remark,
|
|
material_usages=body.material_usages,
|
|
task_status=body.task_status,
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
db.refresh(obj)
|
|
if (obj.task_status or "").strip() == "ywc":
|
|
wo_svc.try_complete_work_order(db, obj.work_order_id, tenant_no)
|
|
elif was_done:
|
|
wo_svc.try_reopen_work_order(db, obj.work_order_id, tenant_no)
|
|
return success_response(
|
|
task_svc._task_to_dict(obj, work_order_no="", db=db, tenant_no=tenant_no),
|
|
message="已保存",
|
|
)
|
|
except HTTPException as e:
|
|
db.rollback()
|
|
detail = e.detail
|
|
msg = detail if isinstance(detail, str) else str(detail)
|
|
return error_response(e.status_code, msg)
|
|
except PermissionError as e:
|
|
db.rollback()
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
db.rollback()
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-tasks/{task_id}/attachments")
|
|
async def update_qc_task_attachments(
|
|
task_id: str,
|
|
body: TaskAttachmentsBody,
|
|
operateScope: Optional[str] = Query(None, alias="operateScope"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_any_menu_code("biz:qcTask:list", "biz:myQcTask:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = task_svc.update_task_attachments(
|
|
db,
|
|
task_id,
|
|
attachments=body.third_party_attachments,
|
|
tenant_no=tenant_no,
|
|
user_id=_get_current_user_id(user),
|
|
)
|
|
return success_response(task_svc._task_to_dict(obj), message="已保存")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-tasks/{task_id}")
|
|
async def delete_qc_task_api(
|
|
task_id: str,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
task_svc.delete_qc_task(db, task_id, tenant_no, user.get("user_id"))
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 质检物料库 ----------
|
|
|
|
|
|
def _maybe_strip_qc_material_amount_fields(
|
|
db: Session, user: dict, rows: Iterable[Dict[str, Any]]
|
|
) -> None:
|
|
if user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM):
|
|
return
|
|
strip_row_fields(rows, *QC_MATERIAL_AMOUNT_FIELD_NAMES)
|
|
|
|
|
|
def _user_can_view_qc_material_amount(db: Session, user: dict) -> bool:
|
|
return user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM)
|
|
|
|
|
|
def _strip_amount_from_write_data(db: Session, user: dict, data: Dict[str, Any]) -> None:
|
|
if _user_can_view_qc_material_amount(db, user):
|
|
return
|
|
for key in ("unit_price", "unitPrice", "amount"):
|
|
data.pop(key, None)
|
|
|
|
|
|
def _resolve_stock_in_amounts(
|
|
db: Session, user: dict, unit_price: Any, amount: Any
|
|
) -> tuple:
|
|
if _user_can_view_qc_material_amount(db, user):
|
|
return unit_price, amount
|
|
return 0, 0
|
|
|
|
|
|
def _maybe_strip_qc_third_party_fee_fields(
|
|
db: Session, user: dict, data: Dict[str, Any]
|
|
) -> None:
|
|
if user_has_menu_perm(db, user, QC_THIRD_PARTY_VIEW_AMOUNT_PERM):
|
|
return
|
|
strip_row_fields([data], "thirdPartyFeeTotal")
|
|
third_items = data.get("thirdPartyItems")
|
|
if isinstance(third_items, list):
|
|
strip_row_fields(third_items, "feeAmount")
|
|
tasks = data.get("tasks")
|
|
if isinstance(tasks, list):
|
|
for task in tasks:
|
|
if isinstance(task, dict) and (task.get("taskType") or "").strip() == "dsfjc":
|
|
strip_row_fields([task], "feeAmount")
|
|
work_order = data.get("workOrder")
|
|
if isinstance(work_order, dict):
|
|
strip_row_fields([work_order], "thirdPartyFeeTotal")
|
|
wo_tasks = work_order.get("tasks")
|
|
if isinstance(wo_tasks, list):
|
|
for task in wo_tasks:
|
|
if isinstance(task, dict) and (task.get("taskType") or "").strip() == "dsfjc":
|
|
strip_row_fields([task], "feeAmount")
|
|
|
|
|
|
def _maybe_strip_qc_material_stock_check_detail(db: Session, user: dict, data: Dict[str, Any]) -> None:
|
|
if user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM):
|
|
return
|
|
for key in ("batchLines", "unusedLines"):
|
|
lines = data.get(key)
|
|
if isinstance(lines, list):
|
|
strip_row_fields(lines, *QC_MATERIAL_AMOUNT_FIELD_NAMES)
|
|
|
|
|
|
class QcMaterialBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_code: str = Field(..., alias="materialCode")
|
|
material_name: str = Field(..., alias="materialName")
|
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
warning_qty: Optional[float] = Field(0, alias="warningQty")
|
|
status: str = "1"
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class QcMaterialUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
|
material_name: Optional[str] = Field(None, alias="materialName")
|
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
warning_qty: Optional[float] = Field(None, alias="warningQty")
|
|
status: Optional[str] = None
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StockInBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
stock_in_no: str = Field(..., alias="stockInNo")
|
|
stock_in_date: str = Field(..., alias="stockInDate")
|
|
expire_date: str = Field(..., alias="expireDate")
|
|
catalog_no: str = Field(..., alias="catalogNo")
|
|
batch_no: str = Field(..., alias="batchNo")
|
|
manufacturer: str
|
|
unit_price: float = Field(..., alias="unitPrice")
|
|
amount: float
|
|
qty: float = Field(..., gt=0)
|
|
remark: Optional[str] = None
|
|
stock_in_time: Optional[str] = Field(None, alias="stockInTime")
|
|
|
|
|
|
class BatchStockInItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_id: str = Field(..., alias="materialId")
|
|
stock_in_no: str = Field(..., alias="stockInNo")
|
|
stock_in_date: str = Field(..., alias="stockInDate")
|
|
expire_date: str = Field(..., alias="expireDate")
|
|
catalog_no: str = Field(..., alias="catalogNo")
|
|
batch_no: str = Field(..., alias="batchNo")
|
|
manufacturer: str
|
|
unit_price: float = Field(..., alias="unitPrice")
|
|
amount: float
|
|
qty: float = Field(..., gt=0)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class BatchStockInBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[BatchStockInItem] = Field(..., min_length=1)
|
|
remark: Optional[str] = None
|
|
stock_in_time: Optional[str] = Field(None, alias="stockInTime")
|
|
|
|
|
|
class QcMonthTemplateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_id: Optional[str] = Field(None, alias="materialId")
|
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
|
material_name: Optional[str] = Field(None, alias="materialName")
|
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
catalog_no: Optional[str] = Field(None, alias="catalogNo")
|
|
batch_no: Optional[str] = Field(None, alias="batchNo")
|
|
manufacturer: Optional[str] = None
|
|
unit_price: Optional[float] = Field(None, alias="unitPrice")
|
|
amount: Optional[float] = None
|
|
in_qty: Optional[float] = Field(None, alias="inQty")
|
|
expire_date: Optional[str] = Field(None, alias="expireDate")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class QcMonthTemplatePreviewBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
stock_in_date: str = Field(..., alias="stockInDate")
|
|
|
|
|
|
class QcMonthTemplateConfirmItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
material_id: Optional[str] = Field(None, alias="materialId")
|
|
material_code: str = Field(..., alias="materialCode")
|
|
material_name: str = Field(..., alias="materialName")
|
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
stock_in_no: str = Field(..., alias="stockInNo")
|
|
expire_date: str = Field(..., alias="expireDate")
|
|
catalog_no: str = Field(..., alias="catalogNo")
|
|
batch_no: str = Field(..., alias="batchNo")
|
|
manufacturer: str
|
|
unit_price: float = Field(..., alias="unitPrice")
|
|
amount: float
|
|
qty: float = Field(..., gt=0)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class QcMonthTemplateConfirmBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
stock_in_date: str = Field(..., alias="stockInDate")
|
|
items: List[QcMonthTemplateConfirmItem] = Field(..., min_length=1)
|
|
remark: Optional[str] = None
|
|
stock_in_time: Optional[str] = Field(None, alias="stockInTime")
|
|
|
|
|
|
class BatchStockOutItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
batch_id: str = Field(..., alias="batchId")
|
|
qty: float = Field(..., gt=0)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class BatchStockOutBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[BatchStockOutItem] = Field(..., min_length=1)
|
|
receiver: Optional[str] = None
|
|
purpose: Optional[str] = None
|
|
remark: Optional[str] = None
|
|
stock_out_time: Optional[str] = Field(None, alias="stockOutTime")
|
|
|
|
|
|
class BatchDiscardItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
batch_id: str = Field(..., alias="batchId")
|
|
qty: float = Field(..., gt=0)
|
|
usage_unit: Optional[str] = Field(None, alias="usageUnit")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class BatchDiscardBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[BatchDiscardItem] = Field(..., min_length=1)
|
|
discard_reason: str = Field(..., alias="discardReason")
|
|
remark: Optional[str] = None
|
|
discard_time: Optional[str] = Field(None, alias="discardTime")
|
|
|
|
|
|
class QcMaterialBatchUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
stock_in_no: Optional[str] = Field(None, alias="stockInNo")
|
|
stock_in_date: Optional[str] = Field(None, alias="stockInDate")
|
|
expire_date: Optional[str] = Field(None, alias="expireDate")
|
|
catalog_no: Optional[str] = Field(None, alias="catalogNo")
|
|
batch_no: Optional[str] = Field(None, alias="batchNo")
|
|
manufacturer: Optional[str] = None
|
|
unit_price: Optional[float] = Field(None, alias="unitPrice")
|
|
amount: Optional[float] = None
|
|
in_qty: Optional[float] = Field(None, alias="inQty")
|
|
remaining_qty: Optional[float] = Field(None, alias="remainingQty")
|
|
remark: Optional[str] = None
|
|
qc_attachment_urls: Optional[str] = Field(None, alias="qcAttachmentUrls")
|
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
|
material_name: Optional[str] = Field(None, alias="materialName")
|
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
warning_qty: Optional[float] = Field(None, alias="warningQty")
|
|
status: Optional[str] = None
|
|
|
|
|
|
@router.get("/qc-material-batches")
|
|
async def list_qc_material_batches(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
materialId: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
stockStatus: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = batch_svc.list_batches(
|
|
db,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
material_id=materialId,
|
|
status=status,
|
|
stock_status=stockStatus,
|
|
tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
_maybe_strip_qc_material_amount_fields(db, user, items)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-material-batches/options")
|
|
async def qc_material_batch_options(
|
|
keyword: Optional[str] = None,
|
|
materialId: Optional[str] = None,
|
|
onlyAvailable: bool = Query(True, alias="onlyAvailable"),
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
options = batch_svc.batch_options(
|
|
db,
|
|
tenant_no,
|
|
keyword=keyword,
|
|
material_id=materialId,
|
|
only_available=onlyAvailable,
|
|
)
|
|
_maybe_strip_qc_material_amount_fields(db, user, options)
|
|
return success_response(options)
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-material-batches/{batch_id}")
|
|
async def get_qc_material_batch(
|
|
batch_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
payload = batch_svc.get_batch_detail(db, batch_id, tenant_no=tenant_no)
|
|
_maybe_strip_qc_material_amount_fields(db, user, [payload])
|
|
return success_response(payload)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-material-batches/{batch_id}")
|
|
async def update_qc_material_batch(
|
|
batch_id: str,
|
|
body: QcMaterialBatchUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
data = body.model_dump(exclude_none=True)
|
|
_strip_amount_from_write_data(db, user, data)
|
|
obj = batch_svc.update_batch(
|
|
db,
|
|
batch_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
**data,
|
|
)
|
|
material = mat_svc.get_material(db, obj.material_id, tenant_no=tenant_no)
|
|
payload = batch_svc._batch_row_to_dict(obj, material)
|
|
_maybe_strip_qc_material_amount_fields(db, user, [payload])
|
|
return success_response(
|
|
payload,
|
|
message="更新成功",
|
|
)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-material-batches/{batch_id}")
|
|
async def delete_qc_material_batch(
|
|
batch_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
batch_svc.delete_batch(db, batch_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-materials")
|
|
async def list_qc_materials(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mat_svc.list_materials(
|
|
db, page=page, page_size=pageSize, keyword=keyword, status=status, tenant_no=tenant_no,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-materials/options")
|
|
async def qc_material_options(
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
return success_response(mat_svc.options_list(db, tenant_no=tenant_no))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-materials")
|
|
async def create_qc_material(
|
|
body: QcMaterialBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
obj = mat_svc.create_material(
|
|
db,
|
|
material_code=body.material_code,
|
|
material_name=body.material_name,
|
|
spec_model=body.spec_model,
|
|
unit=body.unit,
|
|
warning_qty=Decimal(str(body.warning_qty))
|
|
if body.warning_qty is not None
|
|
else None,
|
|
status=mat_svc._norm_status(body.status),
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
)
|
|
return success_response(mat_svc._row_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-materials/{material_id}")
|
|
async def update_qc_material(
|
|
material_id: str,
|
|
body: QcMaterialUpdateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
data = body.model_dump(exclude_none=True)
|
|
if "warning_qty" in data:
|
|
data["warning_qty"] = Decimal(str(data["warning_qty"]))
|
|
if "status" in data:
|
|
data["status"] = mat_svc._norm_status(data["status"])
|
|
obj = mat_svc.update_material(
|
|
db,
|
|
material_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
**data,
|
|
)
|
|
return success_response(mat_svc._row_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-materials/{material_id}")
|
|
async def delete_qc_material(
|
|
material_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
mat_svc.soft_delete_material(db, material_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-materials/batch-stock-in")
|
|
async def batch_stock_in_qc_material(
|
|
body: BatchStockInBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.stock_in_time:
|
|
rt = parse_datetime(body.stock_in_time)
|
|
if not rt:
|
|
return error_response(400, "入库时间格式无效")
|
|
operate_time = rt
|
|
items = []
|
|
for x in body.items:
|
|
unit_price, amount = _resolve_stock_in_amounts(
|
|
db, user, x.unit_price, x.amount
|
|
)
|
|
items.append(
|
|
{
|
|
"material_id": x.material_id,
|
|
"materialId": x.material_id,
|
|
"stock_in_no": x.stock_in_no,
|
|
"stockInNo": x.stock_in_no,
|
|
"stock_in_date": x.stock_in_date,
|
|
"stockInDate": x.stock_in_date,
|
|
"expire_date": x.expire_date,
|
|
"expireDate": x.expire_date,
|
|
"catalog_no": x.catalog_no,
|
|
"catalogNo": x.catalog_no,
|
|
"batch_no": x.batch_no,
|
|
"batchNo": x.batch_no,
|
|
"manufacturer": x.manufacturer,
|
|
"unit_price": unit_price,
|
|
"unitPrice": unit_price,
|
|
"amount": amount,
|
|
"qty": x.qty,
|
|
"remark": x.remark,
|
|
}
|
|
)
|
|
count = batch_svc.batch_stock_in(
|
|
db,
|
|
items=items,
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response({"count": count}, message=f"批量入库成功,共 {count} 项")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-materials/batch-stock-out")
|
|
async def batch_stock_out_qc_material(
|
|
body: BatchStockOutBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.stock_out_time:
|
|
rt = parse_datetime(body.stock_out_time)
|
|
if not rt:
|
|
return error_response(400, "出库时间格式无效")
|
|
operate_time = rt
|
|
items = [
|
|
{"batch_id": x.batch_id, "batchId": x.batch_id, "qty": x.qty, "remark": x.remark}
|
|
for x in body.items
|
|
]
|
|
count = batch_svc.batch_stock_out(
|
|
db,
|
|
items=items,
|
|
receiver=body.receiver,
|
|
purpose=body.purpose,
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response({"count": count}, message=f"批量出库成功,共 {count} 项")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-materials/batch-discard")
|
|
async def batch_discard_qc_material(
|
|
body: BatchDiscardBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.discard_time:
|
|
rt = parse_datetime(body.discard_time)
|
|
if not rt:
|
|
return error_response(400, "废弃时间格式无效")
|
|
operate_time = rt
|
|
items = [
|
|
{
|
|
"batch_id": x.batch_id,
|
|
"batchId": x.batch_id,
|
|
"qty": x.qty,
|
|
"usageUnit": x.usage_unit,
|
|
"usage_unit": x.usage_unit,
|
|
"remark": x.remark,
|
|
}
|
|
for x in body.items
|
|
]
|
|
count = batch_svc.batch_discard(
|
|
db,
|
|
items=items,
|
|
discard_reason=body.discard_reason,
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response({"count": count}, message=f"批量废弃成功,共 {count} 项")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-materials/{material_id}/stock-in")
|
|
async def stock_in_qc_material(
|
|
material_id: str,
|
|
body: StockInBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.stock_in_time:
|
|
rt = parse_datetime(body.stock_in_time)
|
|
if not rt:
|
|
return error_response(400, "入库时间格式无效")
|
|
operate_time = rt
|
|
from decimal import Decimal
|
|
|
|
unit_price, amount = _resolve_stock_in_amounts(
|
|
db, user, body.unit_price, body.amount
|
|
)
|
|
obj = batch_svc.stock_in_one(
|
|
db,
|
|
material_id,
|
|
stock_in_no=body.stock_in_no,
|
|
stock_in_date=body.stock_in_date,
|
|
expire_date=body.expire_date,
|
|
catalog_no=body.catalog_no,
|
|
batch_no=body.batch_no,
|
|
manufacturer=body.manufacturer,
|
|
unit_price=unit_price,
|
|
amount=amount,
|
|
qty=Decimal(str(body.qty)),
|
|
remark=body.remark,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response(batch_svc._batch_row_to_dict(obj), message="入库成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-batches/batch-stock-in")
|
|
async def batch_stock_in_qc_material_batches(
|
|
body: BatchStockInBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
"""批量入库(与 /qc-materials/batch-stock-in 等价)。"""
|
|
return await batch_stock_in_qc_material(body, db, user)
|
|
|
|
|
|
@router.post("/qc-material-batches/batch-stock-out")
|
|
async def batch_stock_out_qc_material_batches(
|
|
body: BatchStockOutBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
"""批量出库(与 /qc-materials/batch-stock-out 等价)。"""
|
|
return await batch_stock_out_qc_material(body, db, user)
|
|
|
|
|
|
@router.post("/qc-material-batches/batch-discard")
|
|
async def batch_discard_qc_material_batches(
|
|
body: BatchDiscardBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
"""批量废弃(与 /qc-materials/batch-discard 等价)。"""
|
|
return await batch_discard_qc_material(body, db, user)
|
|
|
|
|
|
# ---------- 月初入库模板 ----------
|
|
|
|
|
|
@router.get("/qc-material-month-templates")
|
|
async def list_qc_month_templates(
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
rows = month_tpl_svc.list_templates(db, tenant_no=tenant_no)
|
|
return success_response(rows)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-month-templates")
|
|
async def create_qc_month_template(
|
|
body: QcMonthTemplateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
unit_price, amount = _resolve_stock_in_amounts(
|
|
db, user, body.unit_price, body.amount
|
|
)
|
|
data = body.model_dump(by_alias=False)
|
|
data["unit_price"] = unit_price if unit_price is not None else 0
|
|
data["amount"] = amount if amount is not None else 0
|
|
row = month_tpl_svc.create_template(
|
|
db, data=data, tenant_no=tenant_no, user_id=user_id
|
|
)
|
|
return success_response(row, message="新增成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-material-month-templates/{row_id}")
|
|
async def update_qc_month_template(
|
|
row_id: str,
|
|
body: QcMonthTemplateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
unit_price, amount = _resolve_stock_in_amounts(
|
|
db, user, body.unit_price, body.amount
|
|
)
|
|
data = body.model_dump(by_alias=False)
|
|
data["unit_price"] = unit_price if unit_price is not None else 0
|
|
data["amount"] = amount if amount is not None else 0
|
|
row = month_tpl_svc.update_template(
|
|
db, row_id, data=data, tenant_no=tenant_no, user_id=user_id
|
|
)
|
|
return success_response(row, message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-material-month-templates/{row_id}")
|
|
async def delete_qc_month_template(
|
|
row_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
month_tpl_svc.soft_delete_template(db, row_id, tenant_no=tenant_no)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-month-templates/preview")
|
|
async def preview_qc_month_templates(
|
|
body: QcMonthTemplatePreviewBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
stock_in_date = _parse_date(body.stock_in_date)
|
|
rows = month_tpl_svc.preview_templates(
|
|
db, tenant_no=tenant_no, stock_in_date=stock_in_date
|
|
)
|
|
return success_response(rows)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-month-templates/confirm")
|
|
async def confirm_qc_month_templates(
|
|
body: QcMonthTemplateConfirmBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
operate_time = datetime.now()
|
|
if body.stock_in_time:
|
|
rt = parse_datetime(body.stock_in_time)
|
|
if not rt:
|
|
return error_response(400, "入库时间格式无效")
|
|
operate_time = rt
|
|
stock_in_date = _parse_date(body.stock_in_date)
|
|
items = []
|
|
for x in body.items:
|
|
unit_price, amount = _resolve_stock_in_amounts(
|
|
db, user, x.unit_price, x.amount
|
|
)
|
|
items.append(
|
|
{
|
|
"material_id": x.material_id,
|
|
"materialId": x.material_id,
|
|
"material_code": x.material_code,
|
|
"materialCode": x.material_code,
|
|
"material_name": x.material_name,
|
|
"materialName": x.material_name,
|
|
"spec_model": x.spec_model,
|
|
"specModel": x.spec_model,
|
|
"unit": x.unit,
|
|
"stock_in_no": x.stock_in_no,
|
|
"stockInNo": x.stock_in_no,
|
|
"expire_date": x.expire_date,
|
|
"expireDate": x.expire_date,
|
|
"catalog_no": x.catalog_no,
|
|
"catalogNo": x.catalog_no,
|
|
"batch_no": x.batch_no,
|
|
"batchNo": x.batch_no,
|
|
"manufacturer": x.manufacturer,
|
|
"unit_price": unit_price,
|
|
"unitPrice": unit_price,
|
|
"amount": amount,
|
|
"qty": x.qty,
|
|
"remark": x.remark,
|
|
}
|
|
)
|
|
count = month_tpl_svc.confirm_month_import(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
stock_in_date=stock_in_date,
|
|
items=items,
|
|
remark=body.remark,
|
|
operate_time=operate_time,
|
|
)
|
|
return success_response({"count": count}, message=f"月初导入成功,共 {count} 项")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 物料变动记录 ----------
|
|
|
|
|
|
@router.get("/qc-material-usages")
|
|
async def list_qc_material_usages(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
materialId: Optional[str] = None,
|
|
changeType: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
keyword: Optional[str] = None,
|
|
stockInNo: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterialUsage:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mu_svc.list_usages(
|
|
db, tenant_no, page=page, page_size=pageSize,
|
|
material_id=materialId, change_type=changeType,
|
|
start_time=startTime, end_time=endTime,
|
|
keyword=keyword, stock_in_no=stockInNo,
|
|
user_info=user,
|
|
)
|
|
_maybe_strip_qc_material_amount_fields(db, user, items)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/my-qc-material-usages")
|
|
async def list_my_qc_material_usages(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
materialId: Optional[str] = None,
|
|
changeType: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
keyword: Optional[str] = None,
|
|
stockInNo: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:myQcMaterialUsage:list")),
|
|
):
|
|
"""当前登录人的物料领用记录(手动出库领用人 / 任务使用填报人)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mu_svc.list_my_usages(
|
|
db,
|
|
tenant_no,
|
|
user_id=_get_current_user_id(user),
|
|
nick_name=(user.get("real_name") or "").strip(),
|
|
page=page,
|
|
page_size=pageSize,
|
|
material_id=materialId,
|
|
change_type=changeType,
|
|
start_time=startTime,
|
|
end_time=endTime,
|
|
keyword=keyword,
|
|
stock_in_no=stockInNo,
|
|
)
|
|
if not user_has_menu_perm(db, user, MY_QC_MATERIAL_USAGE_VIEW_AMOUNT_PERM):
|
|
strip_row_fields(items, "unitPrice", "amount")
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-material-usages/{usage_id}/linked-task-usages")
|
|
async def list_qc_material_linked_task_usages(
|
|
usage_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(
|
|
require_any_menu_code("biz:qcMaterialUsage:list", "biz:myQcMaterialUsage:list")
|
|
),
|
|
):
|
|
"""领用记录下关联的质检任务物料消耗明细。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items = mu_svc.list_linked_task_usages(
|
|
db, usage_id, tenant_no, user_info=user
|
|
)
|
|
if not (
|
|
user_has_menu_perm(db, user, QC_MATERIAL_VIEW_AMOUNT_PERM)
|
|
or user_has_menu_perm(db, user, MY_QC_MATERIAL_USAGE_VIEW_AMOUNT_PERM)
|
|
):
|
|
strip_row_fields(items, *QC_MATERIAL_AMOUNT_FIELD_NAMES)
|
|
return success_response(items)
|
|
except HTTPException as e:
|
|
detail = e.detail
|
|
msg = detail if isinstance(detail, str) else str(detail)
|
|
return error_response(e.status_code, msg)
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-material-stock-check-requisitions")
|
|
async def list_qc_material_stock_check_requisitions(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(50, ge=1, le=200),
|
|
keyword: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
"""盘点登记:待处理领用记录(未标记已使用完)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = mu_svc.list_stock_check_requisitions(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
user_info=user,
|
|
)
|
|
_maybe_strip_qc_material_amount_fields(db, user, items)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-usages/{usage_id}/mark-fully-used")
|
|
async def mark_qc_material_usage_fully_used(
|
|
usage_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(
|
|
require_any_menu_code("biz:qcMaterial:list", "biz:qcMaterialUsage:list")
|
|
),
|
|
):
|
|
"""标记领用记录已使用完(盘点登记 / 变动记录)。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
mu_svc.mark_requisition_fully_used(
|
|
db,
|
|
usage_id,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
user_info=user,
|
|
)
|
|
return success_response(None, message="已标记为使用完")
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 库存盘点 ----------
|
|
|
|
|
|
class StockCheckCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
title: str
|
|
check_time: Optional[str] = Field(None, alias="checkTime")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StockCheckUnusedLineItem(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
usage_id: Optional[str] = Field(None, alias="usageId")
|
|
batch_id: Optional[str] = Field(None, alias="batchId")
|
|
material_code: Optional[str] = Field(None, alias="materialCode")
|
|
material_name: str = Field(..., alias="materialName")
|
|
spec_model: Optional[str] = Field(None, alias="specModel")
|
|
unit: Optional[str] = None
|
|
stock_in_no: Optional[str] = Field(None, alias="stockInNo")
|
|
stock_in_date: Optional[str] = Field(None, alias="stockInDate")
|
|
expire_date: Optional[str] = Field(None, alias="expireDate")
|
|
catalog_no: Optional[str] = Field(None, alias="catalogNo")
|
|
batch_no: Optional[str] = Field(None, alias="batchNo")
|
|
manufacturer: Optional[str] = None
|
|
unit_price: Optional[float] = Field(None, alias="unitPrice")
|
|
holder: Optional[str] = None
|
|
unused_qty: float = Field(..., alias="unusedQty", gt=0)
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class StockCheckUnusedLinesBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
items: List[StockCheckUnusedLineItem] = Field(default_factory=list)
|
|
|
|
|
|
@router.get("/qc-material-stock-checks")
|
|
async def list_qc_material_stock_checks(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
startTime: Optional[str] = None,
|
|
endTime: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = stock_check_svc.list_checks(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
status=status,
|
|
start_time=startTime,
|
|
end_time=endTime,
|
|
user_info=user,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except PermissionError as e:
|
|
return error_response(403, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-stock-checks")
|
|
async def create_qc_material_stock_check(
|
|
body: StockCheckCreateBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
res = stock_check_svc.create_check(
|
|
db,
|
|
title=body.title,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
check_time=body.check_time,
|
|
remark=body.remark,
|
|
)
|
|
return success_response(res, message="盘点单创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.get("/qc-material-stock-checks/{check_id}")
|
|
async def get_qc_material_stock_check(
|
|
check_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
data = stock_check_svc.get_check_detail(db, check_id, tenant_no)
|
|
_maybe_strip_qc_material_stock_check_detail(db, user, data)
|
|
return success_response(data)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/qc-material-stock-checks/{check_id}/unused-lines")
|
|
async def save_qc_material_stock_check_unused_lines(
|
|
check_id: str,
|
|
body: StockCheckUnusedLinesBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
dept_id = user.get("deptId") or user.get("dept_id") or ""
|
|
count = stock_check_svc.save_unused_lines(
|
|
db,
|
|
check_id,
|
|
items=[x.model_dump(by_alias=False) for x in body.items],
|
|
tenant_no=tenant_no,
|
|
user_id=user_id,
|
|
dept_id=dept_id,
|
|
)
|
|
return success_response({"count": count}, message="登记保存成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-stock-checks/{check_id}/complete")
|
|
async def complete_qc_material_stock_check(
|
|
check_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
stock_check_svc.complete_check(db, check_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="盘点已完成")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-stock-checks/{check_id}/cancel")
|
|
async def cancel_qc_material_stock_check(
|
|
check_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code("biz:qcMaterial:list")),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
user_id = _get_current_user_id(user)
|
|
stock_check_svc.cancel_check(db, check_id, tenant_no=tenant_no, user_id=user_id)
|
|
return success_response(None, message="盘点已作废")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------- 质检物料金额查看授权 ----------
|
|
|
|
|
|
class QcMaterialAmountAuthGrantBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
user_id: str = Field(..., alias="userId")
|
|
|
|
|
|
class QcMaterialAmountAuthBatchBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
user_ids: List[str] = Field(..., alias="userIds", min_length=1)
|
|
|
|
|
|
@router.get("/qc-material-amount-auth/users")
|
|
async def list_qc_material_amount_auth_users(
|
|
page: int = Query(1, ge=1),
|
|
pageSize: int = Query(10, ge=1, le=100),
|
|
keyword: Optional[str] = None,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
|
|
):
|
|
"""已授权「质检物料金额查看」角色的用户列表。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = amount_auth_svc.list_authorized_users(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
page=page,
|
|
page_size=pageSize,
|
|
keyword=keyword,
|
|
)
|
|
return success_response(_page_payload(items, total, page, pageSize))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-amount-auth/users")
|
|
async def grant_qc_material_amount_auth(
|
|
body: QcMaterialAmountAuthGrantBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
|
|
):
|
|
"""为用户绑定「质检物料金额查看」专用角色。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
operator_id = _get_current_user_id(user)
|
|
created = amount_auth_svc.grant_amount_view_role(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
user_id=body.user_id.strip(),
|
|
operator_id=operator_id,
|
|
)
|
|
msg = "授权成功" if created else "该用户已授权"
|
|
return success_response({"created": created}, message=msg)
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/qc-material-amount-auth/users/{user_id}")
|
|
async def revoke_qc_material_amount_auth(
|
|
user_id: str,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
|
|
):
|
|
"""解除用户的「质检物料金额查看」专用角色。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
operator_id = _get_current_user_id(user)
|
|
removed = amount_auth_svc.revoke_amount_view_role(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
user_id=user_id.strip(),
|
|
operator_id=operator_id,
|
|
)
|
|
if not removed:
|
|
return error_response(400, "该用户未授权或已解除")
|
|
return success_response(None, message="已解除授权")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.post("/qc-material-amount-auth/users/batch-revoke")
|
|
async def batch_revoke_qc_material_amount_auth(
|
|
body: QcMaterialAmountAuthBatchBody,
|
|
db: Session = Depends(get_db),
|
|
user: dict = Depends(require_menu_code(QC_MATERIAL_AMOUNT_AUTH_LIST_PERM)),
|
|
):
|
|
"""批量解除「质检物料金额查看」授权。"""
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
operator_id = _get_current_user_id(user)
|
|
count = 0
|
|
for uid in body.user_ids:
|
|
uid = (uid or "").strip()
|
|
if not uid:
|
|
continue
|
|
if amount_auth_svc.revoke_amount_view_role(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
user_id=uid,
|
|
operator_id=operator_id,
|
|
):
|
|
count += 1
|
|
return success_response({"count": count}, message=f"已解除 {count} 项授权")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
except Exception as e:
|
|
return error_response(400, str(e))
|