ForgeFlow-ERP/backend/app/api/routes/quality.py
2026-06-12 16:00:56 +08:00

191 lines
7.8 KiB
Python

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),
)