190 lines
7.4 KiB
Python
190 lines
7.4 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 _resolve_io_order_link_flags(plan: Optional[PlPlan], header) -> tuple[bool, bool]:
|
|
"""返回 (作废, 归档)。现场链接实际走入库/出库单公共页。"""
|
|
voided = header.status == "voided" or bool(plan and plan.follow_status == "cancelled")
|
|
archived = bool(plan and plan.archive_status == "archived")
|
|
return voided, archived
|
|
|
|
|
|
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, archived = _resolve_io_order_link_flags(plan, header)
|
|
data = order_header_to_dict(
|
|
kind, header, items, db=db, public_unit_as_label=True
|
|
)
|
|
data["voided"] = voided
|
|
data["archived"] = archived
|
|
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, "单据已作废,无法提交")
|
|
if page["archived"]:
|
|
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 page["archived"]:
|
|
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)
|