Files
shanxia2.0/backend/app/api/pl/public_io_order.py
T

178 lines
6.8 KiB
Python

"""运输计划 · 入库/出库单现场免登录公共 API"""
from __future__ import annotations
import os
from decimal import Decimal
from typing import Optional
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from sqlalchemy.orm import Session
from app.core.attachment_biz import resolve_upload_folder
from app.core.config import settings
from app.core.database import get_db
from app.core.response import error_response, success_response
from app.core.storage import storage
from app.crud import oss as oss_svc
from app.models.pl_models import (
PlInboundOrder,
PlInboundOrderItem,
PlOutboundOrder,
PlOutboundOrderItem,
PlPlan,
)
from app.schemas.pl.io_order import PublicIoOrderFeedback
from app.services.pl_io_order_service import apply_site_feedback, order_header_to_dict, parse_feedback_images
inbound_public_router = APIRouter(prefix="/public/pl/inbound", tags=["运输计划-入库单公共"])
outbound_public_router = APIRouter(prefix="/public/pl/outbound", tags=["运输计划-出库单公共"])
def _models(kind: str):
if kind == "inbound":
return PlInboundOrder, PlInboundOrderItem
return PlOutboundOrder, PlOutboundOrderItem
def _get_by_token(db: Session, kind: str, token: str):
header_m, item_m = _models(kind)
header = (
db.query(header_m)
.filter(header_m.site_access_token == token, header_m.del_flag == 0)
.first()
)
if not header:
raise HTTPException(status_code=404, detail="链接无效或已失效")
items = (
db.query(item_m)
.filter(item_m.order_id == header.id, item_m.del_flag == 0)
.order_by(item_m.order_num.asc())
.all()
)
return header, items
def _build_page(db: Session, kind: str, header, items) -> dict:
plan = db.query(PlPlan).filter(PlPlan.id == header.plan_id, PlPlan.del_flag == 0).first()
voided = header.status == "voided" or bool(plan and plan.follow_status == "cancelled")
data = order_header_to_dict(
kind, header, items, db=db, public_unit_as_label=True
)
data["voided"] = voided
data["title"] = "入库单" if kind == "inbound" else "出库单"
return data
def _format_file_size(size_bytes: int) -> str:
if size_bytes < 1024:
return f"{size_bytes} B"
if size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
return f"{size_bytes / (1024 * 1024):.1f} MB"
def _make_handlers(kind: str):
def get_page(token: str, db: Session = Depends(get_db)):
header, items = _get_by_token(db, kind, token)
return success_response(_build_page(db, kind, header, items))
def save_feedback(token: str, payload: PublicIoOrderFeedback, db: Session = Depends(get_db)):
header, items = _get_by_token(db, kind, token)
page = _build_page(db, kind, header, items)
if page["voided"]:
return error_response(400, "单据已作废,无法提交")
qty_map = {x.id: Decimal(str(x.actual_qty)) for x in payload.items}
brand_qty_map: dict[str, list[dict]] | None = None
if any(x.brand_qtys is not None for x in payload.items):
brand_qty_map = {}
for x in payload.items:
if x.brand_qtys is not None:
brand_qty_map[x.id] = [
b.model_dump(by_alias=True) for b in x.brand_qtys
]
images = payload.images
if not images:
images = parse_feedback_images(header.site_feedback_images)
try:
apply_site_feedback(
db,
kind=kind,
header=header,
items=items,
item_qty=qty_map,
remark=payload.remark,
images=images,
status=payload.status,
item_brand_qtys=brand_qty_map,
enforce_brand=(payload.status == "completed"),
)
except ValueError as exc:
return error_response(400, str(exc))
db.commit()
return success_response({"success": True, "message": "保存成功"})
def upload_attachment(
token: str,
file: UploadFile = File(...),
folder: Optional[str] = Form(""),
db: Session = Depends(get_db),
):
header, _items = _get_by_token(db, kind, token)
page = _build_page(db, kind, header, _items)
if page["voided"]:
return error_response(400, "单据已作废,无法上传附件")
if not file or not file.filename:
return error_response(400, "请选择文件")
try:
effective_folder = resolve_upload_folder(
folder=folder or "", biz_type="pl_io_order_site_feedback"
)
file_content = file.file.read()
full_url = storage.upload_file(file_content, file.filename, effective_folder)
base_url = settings.MINIO_BASE_URL.rstrip("/") if settings.MINIO_BASE_URL else ""
if base_url and full_url.startswith(base_url + "/"):
file_url = full_url[len(base_url) + 1 :]
else:
file_url = full_url
record = oss_svc.create_oss(
db,
tenant_no=header.tenant_no,
url=file_url,
original_name=file.filename,
file_suffix=os.path.splitext(file.filename)[1].lstrip(".") if file.filename else "",
file_size=len(file_content),
file_type=file.content_type or "",
biz_type="pl_io_order_site_feedback",
user_id=None,
ext1=effective_folder or None,
)
db.commit()
return success_response(
{
"id": record.id,
"fileName": record.original_name,
"fileUrl": record.url,
"fileSize": record.file_size,
"fileSizeDisplay": _format_file_size(record.file_size or 0),
"fileType": record.file_type,
}
)
except Exception as exc:
db.rollback()
return error_response(500, f"上传失败: {str(exc)}")
get_page.__name__ = f"get_public_{kind}_order"
save_feedback.__name__ = f"save_public_{kind}_feedback"
upload_attachment.__name__ = f"upload_public_{kind}_attachment"
return get_page, save_feedback, upload_attachment
_in_get, _in_fb, _in_up = _make_handlers("inbound")
_out_get, _out_fb, _out_up = _make_handlers("outbound")
inbound_public_router.get("/{token}")(_in_get)
inbound_public_router.post("/{token}/feedback")(_in_fb)
inbound_public_router.post("/{token}/attachments")(_in_up)
outbound_public_router.get("/{token}")(_out_get)
outbound_public_router.post("/{token}/feedback")(_out_fb)
outbound_public_router.post("/{token}/attachments")(_out_up)