| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- """运输计划 · 入库/出库单现场免登录公共 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)
|