feat(location): add paicha overview endpoint
This commit is contained in:
@@ -3,8 +3,12 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.location import LocationRequest, LocationResponse
|
||||
from app.services.location_service import register_location
|
||||
from app.schemas.location import (
|
||||
LocationRequest,
|
||||
LocationResponse,
|
||||
PaichaOverviewResponse,
|
||||
)
|
||||
from app.services.location_service import get_paicha_overview, register_location
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -31,3 +35,21 @@ def create_location(req: LocationRequest, db: Session = Depends(get_db)):
|
||||
created_at=record.created_at.isoformat(),
|
||||
previous_location=getattr(record, "previous_location", None),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/location/paicha-overview",
|
||||
response_model=PaichaOverviewResponse,
|
||||
responses={
|
||||
400: {
|
||||
"description": "总排号格式不合法",
|
||||
"model": ErrorResponse,
|
||||
},
|
||||
404: {
|
||||
"description": "该总排号未找到对应排产号",
|
||||
"model": ErrorResponse,
|
||||
},
|
||||
},
|
||||
)
|
||||
def paicha_overview(zongpai_no: str, db: Session = Depends(get_db)):
|
||||
return get_paicha_overview(db, zongpai_no)
|
||||
|
||||
17
app/main.py
17
app/main.py
@@ -18,7 +18,11 @@ except Exception as e:
|
||||
|
||||
from app.api.v1.location import router as location_router
|
||||
from app.api.v1.box import router as box_router
|
||||
from app.services.location_service import AlreadyOffShelfError, DuplicateLocationError
|
||||
from app.services.location_service import (
|
||||
AlreadyOffShelfError,
|
||||
DuplicateLocationError,
|
||||
PaichaNotFoundError,
|
||||
)
|
||||
from app.services.box_service import (
|
||||
BoxItemNotFoundError,
|
||||
DuplicateBoxItemError,
|
||||
@@ -137,6 +141,17 @@ async def box_item_not_found_handler(request: Request, exc: BoxItemNotFoundError
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(PaichaNotFoundError)
|
||||
async def paicha_not_found_handler(request: Request, exc: PaichaNotFoundError):
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={
|
||||
"error_code": "PAICHA_NOT_FOUND",
|
||||
"message": "暂无排产信息",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(InvalidQuantityError)
|
||||
async def invalid_quantity_handler(request: Request, exc: InvalidQuantityError):
|
||||
return JSONResponse(
|
||||
|
||||
@@ -35,6 +35,20 @@ class LocationResponse(BaseModel):
|
||||
previous_location: str | None = None
|
||||
|
||||
|
||||
class PaichaOverviewItem(BaseModel):
|
||||
zongpai_no: str
|
||||
work_order_no: str | None = None
|
||||
quantity: int
|
||||
location_code: str | None = None
|
||||
status: str
|
||||
|
||||
|
||||
class PaichaOverviewResponse(BaseModel):
|
||||
paicha_no: str
|
||||
total_count: int
|
||||
items: list[PaichaOverviewItem]
|
||||
|
||||
|
||||
class DuplicateLocationDetail(BaseModel):
|
||||
existing_location: str
|
||||
created_at: str
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.finished_goods import FinishedGoodsLocation
|
||||
from app.schemas.location import LocationRequest
|
||||
from app.services.box_service import InvalidZongpaiError
|
||||
|
||||
ZONGPAI_PATTERN = re.compile(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$")
|
||||
|
||||
|
||||
class DuplicateLocationError(Exception):
|
||||
@@ -22,10 +27,117 @@ class AlreadyOffShelfError(Exception):
|
||||
self.registered_at = registered_at
|
||||
|
||||
|
||||
class PaichaNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _is_transit_location(location_code: str) -> bool:
|
||||
return location_code.startswith("TRANS-")
|
||||
|
||||
|
||||
def _erp_info_sql(db: Session) -> str:
|
||||
dialect_name = db.bind.dialect.name if db.bind is not None else ""
|
||||
if dialect_name == "postgresql":
|
||||
return (
|
||||
'SELECT "总排号", "排产号", "数量", "工令号" '
|
||||
'FROM "ERPAuto"."vw_productionContractData" '
|
||||
'WHERE "总排号" = :zongpai_no '
|
||||
"LIMIT 1"
|
||||
)
|
||||
return (
|
||||
"SELECT TOP 1 [总排号], [排产号], [数量], [工令号] "
|
||||
"FROM [ERPAuto].[vw_productionContractData] "
|
||||
"WHERE [总排号] = :zongpai_no"
|
||||
)
|
||||
|
||||
|
||||
def _erp_paicha_items_sql(db: Session) -> str:
|
||||
dialect_name = db.bind.dialect.name if db.bind is not None else ""
|
||||
if dialect_name == "postgresql":
|
||||
return (
|
||||
'SELECT "总排号", "工令号", "数量" '
|
||||
'FROM "ERPAuto"."vw_productionContractData" '
|
||||
'WHERE "排产号" = :paicha_no'
|
||||
)
|
||||
return (
|
||||
"SELECT [总排号], [工令号], [数量] "
|
||||
"FROM [ERPAuto].[vw_productionContractData] "
|
||||
"WHERE [排产号] = :paicha_no"
|
||||
)
|
||||
|
||||
|
||||
def _query_paicha_no(db: Session, zongpai_no: str) -> str:
|
||||
row = db.execute(text(_erp_info_sql(db)), {"zongpai_no": zongpai_no}).fetchone()
|
||||
if not row:
|
||||
raise PaichaNotFoundError()
|
||||
return row[1]
|
||||
|
||||
|
||||
def _query_paicha_items(db: Session, paicha_no: str) -> list[dict]:
|
||||
rows = db.execute(text(_erp_paicha_items_sql(db)), {"paicha_no": paicha_no}).fetchall()
|
||||
return [
|
||||
{
|
||||
"zongpai_no": row[0],
|
||||
"work_order_no": row[1],
|
||||
"quantity": int(row[2]) if row[2] is not None else 0,
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _location_status(location_code: str | None) -> str:
|
||||
if location_code is None:
|
||||
return "not_shelved"
|
||||
if _is_transit_location(location_code):
|
||||
return "transferred"
|
||||
return "on_shelf"
|
||||
|
||||
|
||||
def get_paicha_overview(db: Session, zongpai_no: str) -> dict:
|
||||
zongpai_no = zongpai_no.strip().upper()
|
||||
if not ZONGPAI_PATTERN.match(zongpai_no):
|
||||
raise InvalidZongpaiError()
|
||||
|
||||
paicha_no = _query_paicha_no(db, zongpai_no)
|
||||
erp_items = _query_paicha_items(db, paicha_no)
|
||||
if not erp_items:
|
||||
raise PaichaNotFoundError()
|
||||
|
||||
zongpai_nos = [item["zongpai_no"] for item in erp_items]
|
||||
locations = (
|
||||
db.query(FinishedGoodsLocation)
|
||||
.filter(FinishedGoodsLocation.zongpai_no.in_(zongpai_nos))
|
||||
.all()
|
||||
)
|
||||
location_map = {item.zongpai_no: item.location_code for item in locations}
|
||||
|
||||
status_order = {"on_shelf": 0, "transferred": 1, "not_shelved": 2}
|
||||
overview_items = []
|
||||
for item in erp_items:
|
||||
location_code = location_map.get(item["zongpai_no"])
|
||||
status = _location_status(location_code)
|
||||
overview_items.append({
|
||||
"zongpai_no": item["zongpai_no"],
|
||||
"work_order_no": item["work_order_no"],
|
||||
"quantity": item["quantity"],
|
||||
"location_code": location_code,
|
||||
"status": status,
|
||||
})
|
||||
|
||||
overview_items.sort(
|
||||
key=lambda item: (
|
||||
status_order.get(item["status"], 99),
|
||||
item["zongpai_no"],
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"paicha_no": paicha_no,
|
||||
"total_count": len(overview_items),
|
||||
"items": overview_items,
|
||||
}
|
||||
|
||||
|
||||
def register_location(db: Session, req: LocationRequest) -> FinishedGoodsLocation:
|
||||
existing = (
|
||||
db.query(FinishedGoodsLocation)
|
||||
|
||||
Reference in New Issue
Block a user