from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import and_, select from sqlalchemy.orm import Session from app.db.session import get_db from app.models.operations import PurchaseOrderItem, PurchaseReceiptItem, StockLot from app.models.planning import MaterialDemand from app.schemas.operations import QualityInspectionAction, QualityInspectionRead from app.services.auth import AuthContext, require_authenticated_user from app.services.document_archives import DOCUMENT_TYPE_QUALITY_INSPECTION, generate_document_archive from app.services.operations import ( get_quality_inspections_query, recalc_purchase_order_item_received_weight, sync_purchase_order_status, sync_purchase_receipt_status, to_decimal, upsert_stock_balance, ) router = APIRouter(dependencies=[Depends(require_authenticated_user)]) def _auth_context_user_id(context: AuthContext | object) -> int | None: user = getattr(context, "user", None) user_id = getattr(user, "id", None) return int(user_id) if user_id is not None else None def _read_quality_inspection(db: Session, receipt_item_id: int) -> QualityInspectionRead: row = db.execute(get_quality_inspections_query(limit=1).where(PurchaseReceiptItem.id == receipt_item_id)).mappings().first() if not row: raise HTTPException(status_code=500, detail="质检结果保存后读取失败") return QualityInspectionRead.model_validate(dict(row)) def _read_quality_inspection_after_archive_generation( db: Session, receipt_item_id: int, *, created_by: int | None, ) -> QualityInspectionRead: try: archive_kwargs = {"created_by": created_by} if created_by is not None else {} archive_result = generate_document_archive( db, DOCUMENT_TYPE_QUALITY_INSPECTION, int(receipt_item_id), **archive_kwargs, ) archive_fields = { "archive_status": archive_result.archive_status, "archive_version": archive_result.archive_version, "archive_error_message": archive_result.archive_error_message, } except Exception as exc: try: db.rollback() except Exception: pass archive_fields = { "archive_status": "归档失败", "archive_version": None, "archive_error_message": f"归档失败:{exc}", } return _read_quality_inspection(db, receipt_item_id).model_copy(update=archive_fields) @router.get("/receipt-inspections", response_model=list[QualityInspectionRead]) def list_receipt_inspections( pending_only: bool = Query(default=False), limit: int = Query(default=100, ge=1, le=500), db: Session = Depends(get_db), ) -> list[QualityInspectionRead]: rows = db.execute(get_quality_inspections_query(limit=limit, pending_only=pending_only)).mappings().all() return [QualityInspectionRead.model_validate(dict(row)) for row in rows] @router.post("/receipt-inspections/{receipt_item_id}", response_model=QualityInspectionRead) def inspect_receipt_item( receipt_item_id: int, payload: QualityInspectionAction, context: AuthContext = Depends(require_authenticated_user), db: Session = Depends(get_db), ) -> QualityInspectionRead: receipt_item = db.get(PurchaseReceiptItem, receipt_item_id) if not receipt_item: raise HTTPException(status_code=404, detail="到货明细不存在") lot = db.scalar( select(StockLot).where( StockLot.source_doc_type == "PURCHASE_RECEIPT", StockLot.source_doc_id == receipt_item.receipt_id, StockLot.source_line_id == receipt_item.id, ) ) merged_into_existing_lot = False if not lot and receipt_item.merge_to_lot_id: lot = db.get(StockLot, receipt_item.merge_to_lot_id) merged_into_existing_lot = True if not lot: raise HTTPException(status_code=404, detail="对应批次不存在") po_item = db.get(PurchaseOrderItem, receipt_item.purchase_order_item_id) if not po_item: raise HTTPException(status_code=404, detail="采购订单明细不存在") result = payload.result.upper() if result not in {"PASS", "REJECT"}: raise HTTPException(status_code=400, detail="质检结果只支持 PASS 或 REJECT") if result == "PASS": receipt_qty = to_decimal(receipt_item.received_qty) receipt_weight = to_decimal(receipt_item.received_weight_kg) receipt_item.accepted_qty = receipt_qty receipt_item.accepted_weight_kg = receipt_weight receipt_item.status = "PASSED" lot.quality_status = "PASS" lot.status = "AVAILABLE" if merged_into_existing_lot: lot.locked_qty = max(to_decimal(0), to_decimal(lot.locked_qty) - receipt_qty) lot.locked_weight_kg = max( to_decimal(0), to_decimal(lot.locked_weight_kg) - receipt_weight, ) else: lot.locked_qty = to_decimal(0) lot.locked_weight_kg = to_decimal(0) upsert_stock_balance( db, item_id=lot.item_id, warehouse_id=lot.warehouse_id, location_id=lot.location_id, qty_delta=to_decimal(0), weight_delta=to_decimal(0), available_qty_delta=receipt_qty, available_weight_delta=receipt_weight, unit_cost=to_decimal(lot.unit_cost, "0.0001"), ) else: receipt_item.accepted_qty = to_decimal(0) receipt_item.accepted_weight_kg = to_decimal(0) receipt_item.status = "REJECTED" if merged_into_existing_lot: reject_qty = to_decimal(receipt_item.received_qty) reject_weight = to_decimal(receipt_item.received_weight_kg) lot.inbound_qty = max(to_decimal(0), to_decimal(lot.inbound_qty) - reject_qty) lot.inbound_weight_kg = max(to_decimal(0), to_decimal(lot.inbound_weight_kg) - reject_weight) lot.remaining_qty = max(to_decimal(0), to_decimal(lot.remaining_qty) - reject_qty) lot.remaining_weight_kg = max(to_decimal(0), to_decimal(lot.remaining_weight_kg) - reject_weight) lot.locked_qty = max(to_decimal(0), to_decimal(lot.locked_qty) - reject_qty) lot.locked_weight_kg = max(to_decimal(0), to_decimal(lot.locked_weight_kg) - reject_weight) if lot.remaining_weight_kg <= 0 and lot.remaining_qty <= 0: lot.status = "DEPLETED" upsert_stock_balance( db, item_id=lot.item_id, warehouse_id=lot.warehouse_id, location_id=lot.location_id, qty_delta=-reject_qty, weight_delta=-reject_weight, available_qty_delta=to_decimal(0), available_weight_delta=to_decimal(0), unit_cost=to_decimal(lot.unit_cost, "0.0001"), ) else: lot.quality_status = "REJECT" lot.status = "REJECTED" lot.locked_qty = to_decimal(lot.remaining_qty) lot.locked_weight_kg = to_decimal(lot.remaining_weight_kg) receipt_item.remark = payload.remark or receipt_item.remark lot.remark = context.employee.employee_name db.add(receipt_item) db.add(lot) db.flush() po_item = recalc_purchase_order_item_received_weight(db, po_item.id) or po_item if po_item.source_demand_id and po_item.status == "CLOSED": demand = db.get(MaterialDemand, po_item.source_demand_id) if demand: demand.status = "RECEIVED" db.add(demand) sync_purchase_receipt_status(db, receipt_item.receipt_id) sync_purchase_order_status(db, po_item.purchase_order_id) db.commit() return _read_quality_inspection_after_archive_generation( db, receipt_item_id, created_by=_auth_context_user_id(context), )