191 lines
7.8 KiB
Python
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),
|
|
)
|