diff --git a/backend/app/api/qc.py b/backend/app/api/qc.py index d83fee87..dc81458a 100644 --- a/backend/app/api/qc.py +++ b/backend/app/api/qc.py @@ -36,6 +36,8 @@ 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 @@ -1563,6 +1565,55 @@ class BatchStockInBody(BaseModel): 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: str = Field(..., alias="materialCode") + material_name: str = Field(..., alias="materialName") + spec_model: Optional[str] = Field(None, alias="specModel") + unit: Optional[str] = None + catalog_no: str = Field(..., alias="catalogNo") + batch_no: str = Field(..., alias="batchNo") + manufacturer: str + unit_price: float = Field(..., alias="unitPrice") + amount: float + in_qty: float = Field(..., alias="inQty", gt=0) + expire_date: str = Field(..., 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") @@ -2058,6 +2109,174 @@ async def batch_discard_qc_material_batches( 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 + data["amount"] = amount + 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 + data["amount"] = amount + 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)) + + # ---------- 物料变动记录 ----------