提交
This commit is contained in:
1 parent
725774ef0c
commit
c3be9d1dce
176 files changed
+41925
-6342
No files matched your search
@@ -7,6 +7,7 @@ from app.api.leave import router as leave_router
|
||||
from app.api.crm import router as crm_router
|
||||
from app.api.monitor import router as monitor_router
|
||||
from app.api.bx import bx_router
|
||||
from app.api.db import db_router
|
||||
|
||||
__all__ = [
|
||||
"public_router",
|
||||
@@ -17,4 +18,5 @@ __all__ = [
|
||||
"crm_router",
|
||||
"monitor_router",
|
||||
"bx_router",
|
||||
"db_router",
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
"""地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.db import dashboard, inventory, plans, vehicles, weighing
|
||||
|
||||
db_router = APIRouter(tags=["地磅管理"])
|
||||
db_router.include_router(vehicles.router)
|
||||
db_router.include_router(plans.router)
|
||||
db_router.include_router(inventory.router)
|
||||
db_router.include_router(weighing.router)
|
||||
db_router.include_router(dashboard.router)
|
||||
|
||||
|
||||
@db_router.get("/db/health")
|
||||
def db_health():
|
||||
"""P0 健康检查:验证 db 路由已挂载。"""
|
||||
return {"status": "ok", "module": "db"}
|
||||
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.db_auth import DbUserContext, get_db_current_user
|
||||
from app.core.tenant_context import get_tenant_no
|
||||
from app.models.db_models import DbInboundOutbound, DbWeighingSlip
|
||||
from app.schemas.db.dashboard import (
|
||||
DashboardCargoStat,
|
||||
DashboardPartnerCargoStat,
|
||||
DashboardStats,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
@router.get("/stats", response_model=DashboardStats)
|
||||
def get_dashboard_stats(
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> DashboardStats:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
today = date.today()
|
||||
|
||||
today_inbound = float(
|
||||
db.query(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
|
||||
.filter(
|
||||
DbInboundOutbound.date == today,
|
||||
DbInboundOutbound.business_type == "入库",
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
today_outbound = float(
|
||||
db.query(func.coalesce(func.sum(DbInboundOutbound.quantity), 0))
|
||||
.filter(
|
||||
DbInboundOutbound.date == today,
|
||||
DbInboundOutbound.business_type == "出库",
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
abnormal_orders = int(
|
||||
db.query(func.count(DbWeighingSlip.id))
|
||||
.filter(
|
||||
DbWeighingSlip.is_voided == 0,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
DbWeighingSlip.tenant_no == tenant_no,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
return DashboardStats(
|
||||
today_inbound=today_inbound,
|
||||
today_outbound=today_outbound,
|
||||
abnormal_orders=abnormal_orders,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/inbound/by-cargo", response_model=list[DashboardCargoStat])
|
||||
def get_inbound_by_cargo(
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[DashboardCargoStat]:
|
||||
return _cargo_stats(db, user, business_type="入库")
|
||||
|
||||
|
||||
@router.get("/inbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat])
|
||||
def get_inbound_by_partner_cargo(
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[DashboardPartnerCargoStat]:
|
||||
return _partner_cargo_stats(db, user, business_type="入库")
|
||||
|
||||
|
||||
@router.get("/outbound/by-cargo", response_model=list[DashboardCargoStat])
|
||||
def get_outbound_by_cargo(
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[DashboardCargoStat]:
|
||||
return _cargo_stats(db, user, business_type="出库")
|
||||
|
||||
|
||||
@router.get("/outbound/by-partner-cargo", response_model=list[DashboardPartnerCargoStat])
|
||||
def get_outbound_by_partner_cargo(
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[DashboardPartnerCargoStat]:
|
||||
return _partner_cargo_stats(db, user, business_type="出库")
|
||||
|
||||
|
||||
def _cargo_stats(db: Session, user: DbUserContext, *, business_type: str) -> list[DashboardCargoStat]:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
today = date.today()
|
||||
query = text(
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(io.product, ws.cargo_name, '未填写货物') AS label,
|
||||
COALESCE(SUM(io.quantity), 0) AS planned,
|
||||
COALESCE(SUM(ws.net_weight) / 1000.0, 0) AS actual
|
||||
FROM db_inbound_outbound io
|
||||
LEFT JOIN db_weighing_slip ws
|
||||
ON ws.inbound_outbound_id = io.id
|
||||
AND ws.is_voided = 0
|
||||
AND ws.del_flag = 0
|
||||
AND ws.tenant_no = io.tenant_no
|
||||
WHERE io.date = :today
|
||||
AND io.business_type = :business_type
|
||||
AND io.del_flag = 0
|
||||
AND io.tenant_no = :tenant_no
|
||||
GROUP BY COALESCE(io.product, ws.cargo_name, '未填写货物')
|
||||
ORDER BY planned DESC
|
||||
"""
|
||||
)
|
||||
rows = db.execute(
|
||||
query,
|
||||
{"today": today, "business_type": business_type, "tenant_no": tenant_no},
|
||||
).all()
|
||||
return [
|
||||
DashboardCargoStat(label=row.label, planned=float(row.planned), actual=float(row.actual))
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _partner_cargo_stats(
|
||||
db: Session,
|
||||
user: DbUserContext,
|
||||
*,
|
||||
business_type: str,
|
||||
) -> list[DashboardPartnerCargoStat]:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
today = date.today()
|
||||
query = text(
|
||||
"""
|
||||
SELECT
|
||||
CONCAT(
|
||||
COALESCE(io.partner_company, ''),
|
||||
' - ',
|
||||
COALESCE(io.product, ws.cargo_name, '未填写货物')
|
||||
) AS label,
|
||||
COALESCE(SUM(io.quantity), 0) AS planned,
|
||||
COALESCE(SUM(ws.net_weight) / 1000.0, 0) AS actual
|
||||
FROM db_inbound_outbound io
|
||||
LEFT JOIN db_weighing_slip ws
|
||||
ON ws.inbound_outbound_id = io.id
|
||||
AND ws.is_voided = 0
|
||||
AND ws.del_flag = 0
|
||||
AND ws.tenant_no = io.tenant_no
|
||||
WHERE io.date = :today
|
||||
AND io.business_type = :business_type
|
||||
AND io.del_flag = 0
|
||||
AND io.tenant_no = :tenant_no
|
||||
GROUP BY io.partner_company, io.product, ws.cargo_name
|
||||
ORDER BY io.partner_company, planned DESC
|
||||
"""
|
||||
)
|
||||
rows = db.execute(
|
||||
query,
|
||||
{"today": today, "business_type": business_type, "tenant_no": tenant_no},
|
||||
).all()
|
||||
return [
|
||||
DashboardPartnerCargoStat(
|
||||
label=row.label,
|
||||
planned=float(row.planned),
|
||||
actual=float(row.actual),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
@@ -0,0 +1,488 @@
|
||||
from typing import Optional
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.db_auth import DbUserContext, get_db_current_user, get_db_current_user_optional
|
||||
from app.core.tenant_context import get_tenant_no
|
||||
from app.core.utils import generate_uuid
|
||||
from app.services.db.inventory_id import next_inbound_outbound_id
|
||||
from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbVehicle, DbWeighingSlip
|
||||
from app.schemas.db.inventory import (
|
||||
CreateLinkForPlanPayload,
|
||||
InboundOutboundCreate,
|
||||
InboundOutboundRead,
|
||||
InboundOutboundUpdate,
|
||||
)
|
||||
from app.schemas.db.plans import PurchasePlanRead
|
||||
from app.schemas.db.weighing import WeighingSlipRead
|
||||
from app.services.db.mappers import (
|
||||
EMPTY_INVENTORY_DATE,
|
||||
inventory_to_read,
|
||||
is_empty_inventory_date,
|
||||
parse_date_str,
|
||||
plan_to_read,
|
||||
weighing_to_read,
|
||||
)
|
||||
from app.services.db.product_helpers import (
|
||||
apply_product_snapshot_to_inventory,
|
||||
resolve_crm_product,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/inventory", tags=["inventory"])
|
||||
|
||||
STATUS_THIRD_PARTY_FILL = "待三方填写"
|
||||
_INVENTORY_REMARK_FIELD = "remarks"
|
||||
|
||||
|
||||
def _base_query(db: Session, tenant_no: str):
|
||||
return db.query(DbInboundOutbound).filter(
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
)
|
||||
|
||||
|
||||
def _generate_batch_no(db: Session, tenant_no: str) -> str:
|
||||
rows = (
|
||||
db.query(DbInboundOutbound.batch_no)
|
||||
.filter(
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
DbInboundOutbound.batch_no.like("CRKDH%"),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
max_seq = 0
|
||||
pattern = re.compile(r"^CRKDH(\d+)$")
|
||||
for (batch_no,) in rows:
|
||||
if batch_no:
|
||||
match = pattern.match(batch_no)
|
||||
if match:
|
||||
max_seq = max(max_seq, int(match.group(1)))
|
||||
return f"CRKDH{max_seq + 1:07d}"
|
||||
|
||||
|
||||
def _resolve_tenant_no(
|
||||
db: Session,
|
||||
user: Optional[DbUserContext],
|
||||
purchase_plan_id: Optional[str] = None,
|
||||
) -> str:
|
||||
if user:
|
||||
return get_tenant_no(db, user.to_tenant_user_dict())
|
||||
if purchase_plan_id:
|
||||
plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(DbPurchasePlan.id == purchase_plan_id, DbPurchasePlan.del_flag == 0)
|
||||
.one_or_none()
|
||||
)
|
||||
if plan:
|
||||
return plan.tenant_no
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="无法确定租户")
|
||||
|
||||
|
||||
def _get_inventory_by_id(db: Session, inventory_id: str) -> DbInboundOutbound:
|
||||
row = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(DbInboundOutbound.id == inventory_id, DbInboundOutbound.del_flag == 0)
|
||||
.one_or_none()
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Inventory {inventory_id} not found",
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def _get_inventory(db: Session, tenant_no: str, inventory_id: str) -> DbInboundOutbound:
|
||||
row = (
|
||||
_base_query(db, tenant_no)
|
||||
.filter(DbInboundOutbound.id == inventory_id)
|
||||
.one_or_none()
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Inventory {inventory_id} not found",
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def _ensure_vehicle_exists(
|
||||
db: Session,
|
||||
tenant_no: str,
|
||||
vehicle_no: str,
|
||||
*,
|
||||
create_by: Optional[str] = None,
|
||||
create_dept_id: Optional[str] = None,
|
||||
) -> None:
|
||||
if not (vehicle_no and vehicle_no.strip()):
|
||||
return
|
||||
normalized = vehicle_no.strip().lower()
|
||||
exists = (
|
||||
db.query(DbVehicle)
|
||||
.filter(
|
||||
DbVehicle.tenant_no == tenant_no,
|
||||
DbVehicle.del_flag == 0,
|
||||
func.lower(DbVehicle.vehicle_no) == normalized,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if exists:
|
||||
return
|
||||
db.add(
|
||||
DbVehicle(
|
||||
id=generate_uuid(),
|
||||
tenant_no=tenant_no,
|
||||
vehicle_no=vehicle_no.strip(),
|
||||
remark=None,
|
||||
create_by=create_by,
|
||||
create_dept_id=create_dept_id or "",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _apply_inventory_product_update(
|
||||
db: Session, tenant_no: str, row: DbInboundOutbound, payload_dict: dict
|
||||
) -> None:
|
||||
if "product" in payload_dict and "product_id" not in payload_dict:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="请从 CRM 重新选择产品",
|
||||
)
|
||||
product_id = payload_dict.pop("product_id", None)
|
||||
payload_dict.pop("product", None)
|
||||
payload_dict.pop("specification", None)
|
||||
if product_id is not None:
|
||||
snapshot = resolve_crm_product(db, tenant_no, product_id)
|
||||
apply_product_snapshot_to_inventory(row, snapshot)
|
||||
|
||||
|
||||
def _apply_inventory_update(row: DbInboundOutbound, payload_dict: dict) -> None:
|
||||
for key, value in payload_dict.items():
|
||||
if key == _INVENTORY_REMARK_FIELD:
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
row.remark = value
|
||||
continue
|
||||
if key == "date":
|
||||
if value is None or (isinstance(value, str) and not value.strip()):
|
||||
row.date = EMPTY_INVENTORY_DATE
|
||||
else:
|
||||
row.date = parse_date_str(str(value)) or EMPTY_INVENTORY_DATE
|
||||
continue
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
setattr(row, key, value)
|
||||
|
||||
|
||||
def _maybe_promote_third_party_status(row: DbInboundOutbound) -> None:
|
||||
if row.status != STATUS_THIRD_PARTY_FILL:
|
||||
return
|
||||
has_date = not is_empty_inventory_date(row.date)
|
||||
has_qty = row.quantity is not None and row.quantity != 0
|
||||
has_vehicle = bool(row.vehicle_no and row.vehicle_no.strip())
|
||||
if has_date and has_qty and has_vehicle:
|
||||
row.status = "待入库" if row.business_type == "入库" else "待出库"
|
||||
|
||||
|
||||
@router.get("/", response_model=list[InboundOutboundRead])
|
||||
def list_inventory(
|
||||
q: Optional[str] = Query(default=None, description="模糊搜索关键词"),
|
||||
plan_no: Optional[str] = Query(default=None, alias="planNo", description="计划号"),
|
||||
batch_no: Optional[str] = Query(default=None, alias="batchNo", description="出入库单号"),
|
||||
partner_company: Optional[str] = Query(
|
||||
default=None, alias="partnerCompany", description="往来单位"
|
||||
),
|
||||
product: Optional[str] = Query(default=None, description="产品"),
|
||||
vehicle_no: Optional[str] = Query(default=None, alias="vehicleNo", description="车牌号"),
|
||||
date_from: Optional[str] = Query(default=None, alias="dateFrom", description="开始日期"),
|
||||
date_to: Optional[str] = Query(default=None, alias="dateTo", description="结束日期"),
|
||||
status_filter: Optional[str] = Query(default=None, alias="status", description="状态筛选"),
|
||||
business_type: Optional[str] = Query(default=None, alias="businessType", description="业务类型"),
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[InboundOutboundRead]:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
query = _base_query(db, tenant_no).order_by(
|
||||
DbInboundOutbound.date.desc(),
|
||||
DbInboundOutbound.id.desc(),
|
||||
)
|
||||
|
||||
if q:
|
||||
keyword = q.lower()
|
||||
query = query.filter(
|
||||
or_(
|
||||
func.lower(DbInboundOutbound.partner_company).contains(keyword),
|
||||
func.lower(DbInboundOutbound.product).contains(keyword),
|
||||
func.lower(DbInboundOutbound.plan_no).contains(keyword),
|
||||
func.lower(DbInboundOutbound.batch_no).contains(keyword),
|
||||
func.lower(DbInboundOutbound.vehicle_no).contains(keyword),
|
||||
)
|
||||
)
|
||||
|
||||
if plan_no:
|
||||
query = query.filter(DbInboundOutbound.plan_no.contains(plan_no))
|
||||
|
||||
if batch_no:
|
||||
query = query.filter(DbInboundOutbound.batch_no.contains(batch_no))
|
||||
|
||||
if partner_company:
|
||||
query = query.filter(
|
||||
func.lower(DbInboundOutbound.partner_company).contains(partner_company.lower())
|
||||
)
|
||||
|
||||
if product:
|
||||
query = query.filter(func.lower(DbInboundOutbound.product).contains(product.lower()))
|
||||
|
||||
if vehicle_no:
|
||||
query = query.filter(DbInboundOutbound.vehicle_no.contains(vehicle_no))
|
||||
|
||||
if date_from:
|
||||
start = parse_date_str(date_from)
|
||||
if start:
|
||||
query = query.filter(DbInboundOutbound.date >= start)
|
||||
|
||||
if date_to:
|
||||
end = parse_date_str(date_to)
|
||||
if end:
|
||||
query = query.filter(DbInboundOutbound.date <= end)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(DbInboundOutbound.status == status_filter)
|
||||
|
||||
if business_type:
|
||||
query = query.filter(DbInboundOutbound.business_type == business_type)
|
||||
|
||||
inventories = query.all()
|
||||
inventory_ids = [inv.id for inv in inventories]
|
||||
weighing_map: dict[str, DbWeighingSlip] = {}
|
||||
if inventory_ids:
|
||||
weighings = (
|
||||
db.query(DbWeighingSlip)
|
||||
.filter(
|
||||
DbWeighingSlip.tenant_no == tenant_no,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
DbWeighingSlip.inbound_outbound_id.in_(inventory_ids),
|
||||
DbWeighingSlip.is_voided == 0,
|
||||
)
|
||||
.order_by(DbWeighingSlip.id.asc())
|
||||
.all()
|
||||
)
|
||||
for weighing in weighings:
|
||||
if (
|
||||
weighing.inbound_outbound_id
|
||||
and weighing.inbound_outbound_id not in weighing_map
|
||||
):
|
||||
weighing_map[weighing.inbound_outbound_id] = weighing
|
||||
|
||||
result_list: list[InboundOutboundRead] = []
|
||||
for inventory in inventories:
|
||||
try:
|
||||
result_list.append(
|
||||
inventory_to_read(
|
||||
inventory,
|
||||
weighing=weighing_map.get(inventory.id),
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
return result_list
|
||||
|
||||
|
||||
@router.post("/", response_model=InboundOutboundRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_inventory(
|
||||
payload: InboundOutboundCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: Optional[DbUserContext] = Depends(get_db_current_user_optional),
|
||||
) -> InboundOutboundRead:
|
||||
tenant_no = _resolve_tenant_no(db, user, payload.purchase_plan_id)
|
||||
|
||||
batch_no = payload.batch_no.strip() if payload.batch_no else None
|
||||
if not batch_no:
|
||||
batch_no = _generate_batch_no(db, tenant_no)
|
||||
|
||||
status_value = (
|
||||
payload.status.strip()
|
||||
if payload.status and str(payload.status).strip()
|
||||
else ("待入库" if payload.business_type == "入库" else "待出库")
|
||||
)
|
||||
|
||||
snapshot = resolve_crm_product(db, tenant_no, payload.product_id)
|
||||
|
||||
row = DbInboundOutbound(
|
||||
id=next_inbound_outbound_id(db, tenant_no),
|
||||
tenant_no=tenant_no,
|
||||
date=parse_date_str(payload.date) or datetime.now().date(),
|
||||
partner_company=payload.partner_company.strip(),
|
||||
business_type=payload.business_type,
|
||||
quantity=payload.quantity,
|
||||
plan_no=payload.plan_no.strip() if payload.plan_no else None,
|
||||
batch_no=batch_no,
|
||||
vehicle_no=payload.vehicle_no.strip(),
|
||||
status=status_value,
|
||||
remark=payload.remarks,
|
||||
purchase_plan_id=payload.purchase_plan_id,
|
||||
create_by=user.id if user else None,
|
||||
create_dept_id=user.dept_id if user else "",
|
||||
)
|
||||
apply_product_snapshot_to_inventory(row, snapshot)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return inventory_to_read(row)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/create-link-for-plan",
|
||||
response_model=InboundOutboundRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_link_for_plan(
|
||||
payload: CreateLinkForPlanPayload,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> InboundOutboundRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(
|
||||
DbPurchasePlan.id == payload.purchase_plan_id,
|
||||
DbPurchasePlan.tenant_no == tenant_no,
|
||||
DbPurchasePlan.del_flag == 0,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if not plan:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found")
|
||||
|
||||
if not plan.product_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="计划未关联 CRM 产品,请先编辑计划并选择产品",
|
||||
)
|
||||
|
||||
row = DbInboundOutbound(
|
||||
id=next_inbound_outbound_id(db, tenant_no),
|
||||
tenant_no=tenant_no,
|
||||
date=EMPTY_INVENTORY_DATE,
|
||||
partner_company=(plan.customer_name or "").strip() or "待填写",
|
||||
business_type=plan.business_type,
|
||||
product_id=plan.product_id,
|
||||
product=(plan.product or "").strip(),
|
||||
specification=plan.specification,
|
||||
quantity=0.0,
|
||||
plan_no=(plan.plan_no or "").strip() or None,
|
||||
batch_no=_generate_batch_no(db, tenant_no),
|
||||
vehicle_no="",
|
||||
status=STATUS_THIRD_PARTY_FILL,
|
||||
remark=payload.remarks,
|
||||
purchase_plan_id=plan.id,
|
||||
create_by=user.id,
|
||||
create_dept_id=user.dept_id,
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return inventory_to_read(row)
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=InboundOutboundRead)
|
||||
def get_inventory(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
) -> InboundOutboundRead:
|
||||
"""单条查询(三方 H5 填写页无需门户登录)。"""
|
||||
row = _get_inventory_by_id(db, id)
|
||||
weighing = (
|
||||
db.query(DbWeighingSlip)
|
||||
.filter(
|
||||
DbWeighingSlip.inbound_outbound_id == row.id,
|
||||
DbWeighingSlip.is_voided == 0,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
)
|
||||
.order_by(DbWeighingSlip.id.asc())
|
||||
.first()
|
||||
)
|
||||
return inventory_to_read(row, weighing=weighing)
|
||||
|
||||
|
||||
@router.patch("/{id}", response_model=InboundOutboundRead)
|
||||
def update_inventory(
|
||||
id: str,
|
||||
payload: InboundOutboundUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
) -> InboundOutboundRead:
|
||||
"""更新(三方 H5 填写页无需门户登录)。"""
|
||||
row = _get_inventory_by_id(db, id)
|
||||
payload_dict = payload.model_dump(exclude_unset=True)
|
||||
|
||||
vehicle_no_updated = payload_dict.get("vehicle_no")
|
||||
if vehicle_no_updated is not None and str(vehicle_no_updated).strip():
|
||||
_ensure_vehicle_exists(db, row.tenant_no, str(vehicle_no_updated).strip())
|
||||
|
||||
_apply_inventory_product_update(db, row.tenant_no, row, payload_dict)
|
||||
_apply_inventory_update(row, payload_dict)
|
||||
_maybe_promote_third_party_status(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return inventory_to_read(row)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_inventory(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> None:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_inventory(db, tenant_no, id)
|
||||
row.del_flag = 1
|
||||
row.update_by = user.id
|
||||
db.commit()
|
||||
|
||||
|
||||
class InventoryRelatedData(BaseModel):
|
||||
plan: Optional[PurchasePlanRead]
|
||||
weighing: Optional[WeighingSlipRead]
|
||||
|
||||
|
||||
@router.get("/{id}/related", response_model=InventoryRelatedData)
|
||||
def get_inventory_related(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
) -> InventoryRelatedData:
|
||||
row = _get_inventory_by_id(db, id)
|
||||
|
||||
plan = None
|
||||
if row.purchase_plan_id:
|
||||
plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(
|
||||
DbPurchasePlan.id == row.purchase_plan_id,
|
||||
DbPurchasePlan.del_flag == 0,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
weighing = (
|
||||
db.query(DbWeighingSlip)
|
||||
.filter(
|
||||
DbWeighingSlip.inbound_outbound_id == row.id,
|
||||
DbWeighingSlip.is_voided == 0,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
)
|
||||
.limit(1)
|
||||
.first()
|
||||
)
|
||||
|
||||
return InventoryRelatedData(
|
||||
plan=plan_to_read(plan) if plan else None,
|
||||
weighing=weighing_to_read(weighing) if weighing else None,
|
||||
)
|
||||
@@ -0,0 +1,475 @@
|
||||
from typing import Optional
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.db_auth import DbUserContext, get_db_current_user
|
||||
from app.core.tenant_context import get_tenant_no
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbWeighingSlip
|
||||
from app.schemas.db.inventory import InboundOutboundRead
|
||||
from app.schemas.db.plans import (
|
||||
PurchasePlanCreate,
|
||||
PurchasePlanRead,
|
||||
PurchasePlanUpdate,
|
||||
PurchasePlanWithFillLink,
|
||||
)
|
||||
from app.schemas.db.weighing import WeighingSlipRead
|
||||
from app.services.db.mappers import (
|
||||
inventory_to_read,
|
||||
parse_date_str,
|
||||
parse_datetime_str,
|
||||
plan_to_read,
|
||||
plan_to_read_with_fill_link,
|
||||
weighing_to_read,
|
||||
)
|
||||
from app.services.db.product_helpers import (
|
||||
apply_product_snapshot_to_plan,
|
||||
resolve_crm_product,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/plans", tags=["plans"])
|
||||
|
||||
STATUS_THIRD_PARTY_FILL = "待三方填写"
|
||||
|
||||
_PLAN_DATE_FIELDS = {"plan_date", "start_date", "end_date"}
|
||||
_PLAN_REMARK_FIELD = "remarks"
|
||||
|
||||
|
||||
def _base_query(db: Session, tenant_no: str):
|
||||
return db.query(DbPurchasePlan).filter(
|
||||
DbPurchasePlan.tenant_no == tenant_no,
|
||||
DbPurchasePlan.del_flag == 0,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[PurchasePlanWithFillLink])
|
||||
def list_plans(
|
||||
q: Optional[str] = Query(default=None, description="模糊搜索往来单位或产品"),
|
||||
plan_no: Optional[str] = Query(default=None, alias="planNo", description="计划号"),
|
||||
customer_name: Optional[str] = Query(
|
||||
default=None, alias="customerName", description="往来单位"
|
||||
),
|
||||
product: Optional[str] = Query(default=None, description="产品"),
|
||||
business_type: Optional[str] = Query(default=None, alias="businessType", description="业务类型"),
|
||||
execution_status: Optional[str] = Query(
|
||||
default=None, alias="executionStatus", description="执行状态"
|
||||
),
|
||||
created_from: Optional[str] = Query(default=None, alias="createdFrom", description="创建开始日期"),
|
||||
created_to: Optional[str] = Query(default=None, alias="createdTo", description="创建结束日期"),
|
||||
start_date_from: Optional[str] = Query(
|
||||
default=None, alias="startDateFrom", description="计划起始日期开始"
|
||||
),
|
||||
start_date_to: Optional[str] = Query(
|
||||
default=None, alias="startDateTo", description="计划起始日期结束"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[PurchasePlanWithFillLink]:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
query = _base_query(db, tenant_no).order_by(DbPurchasePlan.create_time.desc())
|
||||
|
||||
if q:
|
||||
keyword = q.lower()
|
||||
query = query.filter(
|
||||
or_(
|
||||
func.lower(DbPurchasePlan.customer_name).contains(keyword),
|
||||
func.lower(DbPurchasePlan.product).contains(keyword),
|
||||
func.lower(DbPurchasePlan.plan_no).contains(keyword),
|
||||
)
|
||||
)
|
||||
|
||||
if plan_no:
|
||||
query = query.filter(DbPurchasePlan.plan_no.contains(plan_no))
|
||||
|
||||
if customer_name:
|
||||
query = query.filter(
|
||||
func.lower(DbPurchasePlan.customer_name).contains(customer_name.lower())
|
||||
)
|
||||
|
||||
if product:
|
||||
query = query.filter(func.lower(DbPurchasePlan.product).contains(product.lower()))
|
||||
|
||||
if business_type:
|
||||
query = query.filter(DbPurchasePlan.business_type == business_type)
|
||||
|
||||
if execution_status:
|
||||
query = query.filter(DbPurchasePlan.execution_status == execution_status)
|
||||
|
||||
if created_from:
|
||||
query = query.filter(DbPurchasePlan.create_time >= created_from)
|
||||
|
||||
if created_to:
|
||||
query = query.filter(DbPurchasePlan.create_time <= f"{created_to} 23:59:59")
|
||||
|
||||
if start_date_from:
|
||||
start_from = parse_date_str(start_date_from)
|
||||
if start_from:
|
||||
query = query.filter(DbPurchasePlan.start_date >= start_from)
|
||||
|
||||
if start_date_to:
|
||||
start_to = parse_date_str(start_date_to)
|
||||
if start_to:
|
||||
query = query.filter(DbPurchasePlan.start_date <= start_to)
|
||||
|
||||
plans = query.all()
|
||||
if not plans:
|
||||
return []
|
||||
|
||||
plan_ids = [p.id for p in plans]
|
||||
fill_links = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
DbInboundOutbound.purchase_plan_id.in_(plan_ids),
|
||||
DbInboundOutbound.status == STATUS_THIRD_PARTY_FILL,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
plan_id_to_inv_id = {
|
||||
inv.purchase_plan_id: inv.id for inv in fill_links if inv.purchase_plan_id
|
||||
}
|
||||
|
||||
plan_nos = [p.plan_no for p in plans if p.plan_no]
|
||||
executed_sum_map: dict[tuple[Optional[str], Optional[str]], float] = {}
|
||||
|
||||
if plan_ids or plan_nos:
|
||||
weighing_filters = [DbWeighingSlip.is_voided == 0, DbWeighingSlip.del_flag == 0]
|
||||
weighing_filters.append(DbWeighingSlip.tenant_no == tenant_no)
|
||||
id_filters = []
|
||||
if plan_ids:
|
||||
id_filters.append(DbWeighingSlip.purchase_plan_id.in_(plan_ids))
|
||||
if plan_nos:
|
||||
id_filters.append(DbWeighingSlip.plan_no.in_(plan_nos))
|
||||
weighing_filters.append(or_(*id_filters))
|
||||
|
||||
weighing_rows = (
|
||||
db.query(
|
||||
DbWeighingSlip.purchase_plan_id,
|
||||
DbWeighingSlip.plan_no,
|
||||
func.sum(DbWeighingSlip.net_weight).label("total_executed"),
|
||||
)
|
||||
.filter(*weighing_filters)
|
||||
.group_by(DbWeighingSlip.purchase_plan_id, DbWeighingSlip.plan_no)
|
||||
.all()
|
||||
)
|
||||
for row in weighing_rows:
|
||||
key = (row.purchase_plan_id, row.plan_no)
|
||||
executed_sum_map[key] = float(row.total_executed or 0)
|
||||
|
||||
def calc_executed_for_plan(plan: DbPurchasePlan) -> float:
|
||||
if not executed_sum_map:
|
||||
return 0.0
|
||||
total = 0.0
|
||||
for (pid, pno), executed in executed_sum_map.items():
|
||||
if pid is not None and pid == plan.id:
|
||||
total += executed
|
||||
elif pno and plan.plan_no and pno == plan.plan_no:
|
||||
total += executed
|
||||
return total
|
||||
|
||||
result_list: list[PurchasePlanWithFillLink] = []
|
||||
for plan in plans:
|
||||
result_list.append(
|
||||
plan_to_read_with_fill_link(
|
||||
plan,
|
||||
executed_quantity=calc_executed_for_plan(plan),
|
||||
fill_link_inventory_id=plan_id_to_inv_id.get(plan.id),
|
||||
)
|
||||
)
|
||||
return result_list
|
||||
|
||||
|
||||
def _generate_plan_no() -> str:
|
||||
from app.services.db.plan_helpers import generate_plan_no
|
||||
|
||||
return generate_plan_no()
|
||||
|
||||
|
||||
@router.post("/", response_model=PurchasePlanRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_plan(
|
||||
payload: PurchasePlanCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> PurchasePlanRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
plan_no = payload.plan_no if payload.plan_no else _generate_plan_no()
|
||||
|
||||
execution_status = payload.execution_status
|
||||
if not execution_status:
|
||||
execution_status = "待入库" if payload.business_type == "入库" else "待出库"
|
||||
|
||||
snapshot = resolve_crm_product(db, tenant_no, payload.product_id)
|
||||
|
||||
plan = DbPurchasePlan(
|
||||
id=generate_uuid(),
|
||||
tenant_no=tenant_no,
|
||||
plan_no=plan_no,
|
||||
plan_date=parse_date_str(payload.plan_date) or date.today(),
|
||||
start_date=parse_date_str(payload.start_date),
|
||||
end_date=parse_date_str(payload.end_date),
|
||||
business_type=payload.business_type,
|
||||
planned_quantity=payload.planned_quantity,
|
||||
vehicle_no=payload.vehicle_no or "",
|
||||
customer_name=payload.customer_name.strip() if payload.customer_name else None,
|
||||
remark=payload.remarks,
|
||||
execution_status=execution_status,
|
||||
executed_at=None,
|
||||
last_weighing_slip_id=None,
|
||||
executed_quantity=0,
|
||||
auto_generate_enabled=payload.auto_generate_enabled or 0,
|
||||
auto_generate_end_month=payload.auto_generate_end_month,
|
||||
auto_generate_last_month=None,
|
||||
create_by=user.id,
|
||||
create_dept_id=user.dept_id,
|
||||
)
|
||||
apply_product_snapshot_to_plan(plan, snapshot)
|
||||
db.add(plan)
|
||||
db.commit()
|
||||
db.refresh(plan)
|
||||
return plan_to_read(plan)
|
||||
|
||||
|
||||
class AutoGenerateMonthlyResult(BaseModel):
|
||||
created_count: int
|
||||
skipped_reason: Optional[str] = None
|
||||
current_month: str
|
||||
|
||||
|
||||
@router.post("/run-auto-generate", response_model=AutoGenerateMonthlyResult)
|
||||
def run_auto_generate_monthly(
|
||||
force: bool = Query(default=False, description="true 时跳过「仅每月1日」限制,供验收手动触发"),
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> AutoGenerateMonthlyResult:
|
||||
"""手动触发计划按月自动生成(定时任务同款逻辑)。"""
|
||||
get_tenant_no(db, user.to_tenant_user_dict())
|
||||
from app.services.db.auto_generate_plans import run_auto_generate_monthly_plans
|
||||
|
||||
result = run_auto_generate_monthly_plans(db, force=force)
|
||||
return AutoGenerateMonthlyResult(**result)
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=PurchasePlanRead)
|
||||
def get_plan(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> PurchasePlanRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
plan = _get_plan(db, tenant_no, id)
|
||||
return plan_to_read(plan)
|
||||
|
||||
|
||||
@router.patch("/{id}", response_model=PurchasePlanRead)
|
||||
def update_plan(
|
||||
id: str,
|
||||
payload: PurchasePlanUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> PurchasePlanRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
plan = _get_plan(db, tenant_no, id)
|
||||
payload_dict = payload.model_dump(exclude_unset=True)
|
||||
_apply_plan_product_update(db, tenant_no, plan, payload_dict)
|
||||
_apply_plan_update(plan, payload_dict)
|
||||
plan.update_by = user.id
|
||||
db.commit()
|
||||
db.refresh(plan)
|
||||
return plan_to_read(plan)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_plan(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> None:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
plan = _get_plan(db, tenant_no, id)
|
||||
plan.del_flag = 1
|
||||
plan.update_by = user.id
|
||||
db.commit()
|
||||
|
||||
|
||||
def _get_plan(db: Session, tenant_no: str, plan_id: str) -> DbPurchasePlan:
|
||||
plan = (
|
||||
_base_query(db, tenant_no)
|
||||
.filter(DbPurchasePlan.id == plan_id)
|
||||
.one_or_none()
|
||||
)
|
||||
if plan is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Plan {plan_id} not found",
|
||||
)
|
||||
return plan
|
||||
|
||||
|
||||
def _get_plan_by_id_only(db: Session, plan_id: str) -> DbPurchasePlan:
|
||||
plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(DbPurchasePlan.id == plan_id, DbPurchasePlan.del_flag == 0)
|
||||
.one_or_none()
|
||||
)
|
||||
if plan is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Plan {plan_id} not found",
|
||||
)
|
||||
return plan
|
||||
|
||||
|
||||
def _apply_plan_product_update(
|
||||
db: Session, tenant_no: str, plan: DbPurchasePlan, payload_dict: dict
|
||||
) -> None:
|
||||
if "product" in payload_dict and "product_id" not in payload_dict:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="请从 CRM 重新选择产品",
|
||||
)
|
||||
product_id = payload_dict.pop("product_id", None)
|
||||
payload_dict.pop("product", None)
|
||||
payload_dict.pop("specification", None)
|
||||
if product_id is not None:
|
||||
snapshot = resolve_crm_product(db, tenant_no, product_id)
|
||||
apply_product_snapshot_to_plan(plan, snapshot)
|
||||
|
||||
|
||||
def _apply_plan_update(plan: DbPurchasePlan, payload_dict: dict) -> None:
|
||||
for key, value in payload_dict.items():
|
||||
if key == _PLAN_REMARK_FIELD:
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
plan.remark = value
|
||||
continue
|
||||
if key in _PLAN_DATE_FIELDS:
|
||||
if value is None:
|
||||
setattr(plan, key, None)
|
||||
else:
|
||||
setattr(plan, key, parse_date_str(str(value)))
|
||||
continue
|
||||
if key == "executed_at":
|
||||
plan.executed_at = parse_datetime_str(str(value)) if value else None
|
||||
continue
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
setattr(plan, key, value)
|
||||
|
||||
|
||||
class PlanRelatedData(BaseModel):
|
||||
inventories: list[InboundOutboundRead]
|
||||
weighings: list[WeighingSlipRead]
|
||||
|
||||
|
||||
@router.get("/{id}/related", response_model=PlanRelatedData)
|
||||
def get_plan_related(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> PlanRelatedData:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
plan = _get_plan(db, tenant_no, id)
|
||||
|
||||
inventories = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
DbInboundOutbound.purchase_plan_id == plan.id,
|
||||
)
|
||||
.order_by(DbInboundOutbound.date.desc(), DbInboundOutbound.id.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
weighing_query = db.query(DbWeighingSlip).filter(
|
||||
DbWeighingSlip.tenant_no == tenant_no,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
DbWeighingSlip.is_voided == 0,
|
||||
)
|
||||
if plan.plan_no:
|
||||
weighing_query = weighing_query.filter(
|
||||
or_(
|
||||
DbWeighingSlip.plan_no == plan.plan_no,
|
||||
DbWeighingSlip.purchase_plan_id == plan.id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
weighing_query = weighing_query.filter(DbWeighingSlip.purchase_plan_id == plan.id)
|
||||
weighings = weighing_query.order_by(DbWeighingSlip.create_time.desc()).all()
|
||||
|
||||
return PlanRelatedData(
|
||||
inventories=[inventory_to_read(inv) for inv in inventories],
|
||||
weighings=[weighing_to_read(w) for w in weighings],
|
||||
)
|
||||
|
||||
|
||||
class ThirdPartyPlanPageData(BaseModel):
|
||||
plan: PurchasePlanRead
|
||||
today_inventories: list[InboundOutboundRead]
|
||||
month_inventories: list[InboundOutboundRead]
|
||||
|
||||
|
||||
@router.get("/{id}/third-party-page", response_model=ThirdPartyPlanPageData)
|
||||
def get_third_party_plan_page(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
) -> ThirdPartyPlanPageData:
|
||||
"""三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。"""
|
||||
plan = _get_plan_by_id_only(db, id)
|
||||
tenant_no = plan.tenant_no
|
||||
|
||||
today = date.today()
|
||||
start = plan.start_date
|
||||
end = plan.end_date
|
||||
|
||||
if start and today < start:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="计划尚未生效,当前日期不允许填写出入库单",
|
||||
)
|
||||
if end and today > end:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="计划已过期,当前日期不允许填写出入库单",
|
||||
)
|
||||
|
||||
today_str = today.strftime("%Y-%m-%d")
|
||||
today_date = parse_date_str(today_str)
|
||||
inventories = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
DbInboundOutbound.purchase_plan_id == plan.id,
|
||||
DbInboundOutbound.date == today_date,
|
||||
)
|
||||
.order_by(DbInboundOutbound.date.desc(), DbInboundOutbound.id.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
month_start = today.replace(day=1)
|
||||
month_history = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
DbInboundOutbound.purchase_plan_id == plan.id,
|
||||
DbInboundOutbound.date >= month_start,
|
||||
DbInboundOutbound.date < today,
|
||||
)
|
||||
.order_by(DbInboundOutbound.date.desc(), DbInboundOutbound.id.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
return ThirdPartyPlanPageData(
|
||||
plan=plan_to_read(plan),
|
||||
today_inventories=[inventory_to_read(inv) for inv in inventories],
|
||||
month_inventories=[inventory_to_read(inv) for inv in month_history],
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.db_auth import DbUserContext, get_db_current_user
|
||||
from app.core.tenant_context import get_tenant_no
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.db_models import DbVehicle
|
||||
from app.schemas.db.master_data import VehicleCreate, VehicleRead, VehicleUpdate
|
||||
from app.services.db.mappers import vehicle_to_read
|
||||
|
||||
router = APIRouter(prefix="/master-data/vehicles", tags=["vehicles"])
|
||||
|
||||
|
||||
def _base_query(db: Session, tenant_no: str):
|
||||
return db.query(DbVehicle).filter(
|
||||
DbVehicle.tenant_no == tenant_no,
|
||||
DbVehicle.del_flag == 0,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[VehicleRead])
|
||||
def list_vehicles(
|
||||
q: Optional[str] = Query(default=None, description="模糊搜索车牌号"),
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[VehicleRead]:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
query = _base_query(db, tenant_no).order_by(DbVehicle.create_time.desc())
|
||||
if q:
|
||||
keyword = q.lower()
|
||||
query = query.filter(
|
||||
or_(
|
||||
func.lower(DbVehicle.vehicle_no).contains(keyword),
|
||||
func.lower(DbVehicle.remark).contains(keyword),
|
||||
)
|
||||
)
|
||||
rows = query.all()
|
||||
return [vehicle_to_read(row) for row in rows]
|
||||
|
||||
|
||||
@router.get("/{vehicle_id}", response_model=VehicleRead)
|
||||
def get_vehicle(
|
||||
vehicle_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> VehicleRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_vehicle(db, tenant_no, vehicle_id)
|
||||
return vehicle_to_read(row)
|
||||
|
||||
|
||||
@router.post("/", response_model=VehicleRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_vehicle(
|
||||
payload: VehicleCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> VehicleRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
_ensure_unique_vehicle_no(db, tenant_no, payload.vehicle_no)
|
||||
row = DbVehicle(
|
||||
id=generate_uuid(),
|
||||
tenant_no=tenant_no,
|
||||
vehicle_no=payload.vehicle_no.strip(),
|
||||
remark=payload.remarks,
|
||||
create_by=user.id,
|
||||
create_dept_id=user.dept_id,
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return vehicle_to_read(row)
|
||||
|
||||
|
||||
@router.put("/{vehicle_id}", response_model=VehicleRead)
|
||||
def update_vehicle(
|
||||
vehicle_id: str,
|
||||
payload: VehicleUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> VehicleRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_vehicle(db, tenant_no, vehicle_id)
|
||||
if payload.vehicle_no and payload.vehicle_no.strip().lower() != row.vehicle_no.lower():
|
||||
_ensure_unique_vehicle_no(db, tenant_no, payload.vehicle_no)
|
||||
row.vehicle_no = payload.vehicle_no.strip()
|
||||
if payload.remarks is not None:
|
||||
row.remark = payload.remarks
|
||||
row.update_by = user.id
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return vehicle_to_read(row)
|
||||
|
||||
|
||||
@router.patch("/{vehicle_id}", response_model=VehicleRead)
|
||||
def patch_vehicle(
|
||||
vehicle_id: str,
|
||||
payload: VehicleUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> VehicleRead:
|
||||
return update_vehicle(vehicle_id, payload, db, user)
|
||||
|
||||
|
||||
@router.delete("/{vehicle_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_vehicle(
|
||||
vehicle_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> None:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_vehicle(db, tenant_no, vehicle_id)
|
||||
row.del_flag = 1
|
||||
row.update_by = user.id
|
||||
db.commit()
|
||||
|
||||
|
||||
def _get_vehicle(db: Session, tenant_no: str, vehicle_id: str) -> DbVehicle:
|
||||
row = (
|
||||
_base_query(db, tenant_no)
|
||||
.filter(DbVehicle.id == vehicle_id)
|
||||
.one_or_none()
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Vehicle {vehicle_id} not found",
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def _ensure_unique_vehicle_no(db: Session, tenant_no: str, vehicle_no: str) -> None:
|
||||
normalized = vehicle_no.strip().lower()
|
||||
exists = (
|
||||
_base_query(db, tenant_no)
|
||||
.filter(func.lower(DbVehicle.vehicle_no) == normalized)
|
||||
.first()
|
||||
)
|
||||
if exists:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Vehicle number already exists",
|
||||
)
|
||||
@@ -0,0 +1,414 @@
|
||||
from typing import Optional
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.db_auth import DbUserContext, get_db_current_user
|
||||
from app.core.tenant_context import get_tenant_no
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.db_models import DbInboundOutbound, DbPurchasePlan, DbWeighingSlip
|
||||
from app.schemas.db.inventory import InboundOutboundRead
|
||||
from app.schemas.db.plans import PurchasePlanRead
|
||||
from app.schemas.db.weighing import WeighingSlipCreate, WeighingSlipRead, WeighingSlipUpdate
|
||||
from app.services.db.mappers import (
|
||||
datetime_end_of_day,
|
||||
inventory_date_to_api,
|
||||
inventory_to_read,
|
||||
parse_date_str,
|
||||
parse_datetime_str,
|
||||
plan_to_read,
|
||||
weighing_to_read,
|
||||
)
|
||||
from app.services.db.weighing_logic import process_weighing_business_logic
|
||||
from app.services.db.product_helpers import (
|
||||
apply_product_snapshot_to_weighing,
|
||||
resolve_crm_product,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/weighing", tags=["weighing"])
|
||||
|
||||
_WEIGHING_REMARK_FIELD = "remarks"
|
||||
|
||||
|
||||
def _base_query(db: Session, tenant_no: str):
|
||||
return db.query(DbWeighingSlip).filter(
|
||||
DbWeighingSlip.tenant_no == tenant_no,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
DbWeighingSlip.is_voided == 0,
|
||||
)
|
||||
|
||||
|
||||
def _get_weighing(db: Session, tenant_no: str, weighing_id: str) -> DbWeighingSlip:
|
||||
row = (
|
||||
db.query(DbWeighingSlip)
|
||||
.filter(
|
||||
DbWeighingSlip.id == weighing_id,
|
||||
DbWeighingSlip.tenant_no == tenant_no,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Weighing {weighing_id} not found",
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def _apply_weighing_product_update(
|
||||
db: Session, tenant_no: str, row: DbWeighingSlip, payload_dict: dict
|
||||
) -> None:
|
||||
if "cargo_name" in payload_dict and "product_id" not in payload_dict:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="请从 CRM 重新选择产品",
|
||||
)
|
||||
product_id = payload_dict.pop("product_id", None)
|
||||
payload_dict.pop("cargo_name", None)
|
||||
payload_dict.pop("specification", None)
|
||||
if product_id is not None:
|
||||
snapshot = resolve_crm_product(db, tenant_no, product_id)
|
||||
apply_product_snapshot_to_weighing(row, snapshot)
|
||||
|
||||
|
||||
def _apply_weighing_update(row: DbWeighingSlip, payload_dict: dict) -> None:
|
||||
for key, value in payload_dict.items():
|
||||
if key == _WEIGHING_REMARK_FIELD:
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
row.remark = value
|
||||
continue
|
||||
if key in {"gross_weight_time", "tare_weight_time"}:
|
||||
if value is None or (isinstance(value, str) and not str(value).strip()):
|
||||
if key == "tare_weight_time":
|
||||
row.tare_weight_time = None
|
||||
else:
|
||||
parsed = parse_datetime_str(str(value))
|
||||
if parsed:
|
||||
setattr(row, key, parsed)
|
||||
continue
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
setattr(row, key, value)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[WeighingSlipRead])
|
||||
def list_weighing(
|
||||
q: Optional[str] = Query(default=None, description="模糊搜索关键词"),
|
||||
date_from: Optional[str] = Query(default=None, alias="dateFrom", description="开始日期"),
|
||||
date_to: Optional[str] = Query(default=None, alias="dateTo", description="结束日期"),
|
||||
plan_no: Optional[str] = Query(default=None, alias="planNo", description="计划号"),
|
||||
batch_no: Optional[str] = Query(default=None, alias="batchNo", description="出入库单号"),
|
||||
vehicle_no: Optional[str] = Query(default=None, alias="vehicleNo", description="车牌号"),
|
||||
cargo_name: Optional[str] = Query(default=None, alias="cargoName", description="货物名称"),
|
||||
supplier: Optional[str] = Query(default=None, description="供货单位"),
|
||||
consignee: Optional[str] = Query(default=None, description="收货单位"),
|
||||
business_type: Optional[str] = Query(default=None, alias="businessType", description="业务类型"),
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> list[WeighingSlipRead]:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
query = _base_query(db, tenant_no).order_by(DbWeighingSlip.gross_weight_time.desc())
|
||||
|
||||
if q:
|
||||
keyword = q.lower()
|
||||
query = query.filter(
|
||||
or_(
|
||||
func.lower(DbWeighingSlip.vehicle_no).contains(keyword),
|
||||
func.lower(DbWeighingSlip.plan_no).contains(keyword),
|
||||
func.lower(DbWeighingSlip.batch_no).contains(keyword),
|
||||
func.lower(DbWeighingSlip.cargo_name).contains(keyword),
|
||||
func.lower(DbWeighingSlip.supplier).contains(keyword),
|
||||
func.lower(DbWeighingSlip.consignee).contains(keyword),
|
||||
)
|
||||
)
|
||||
|
||||
if date_from:
|
||||
start_date = parse_date_str(date_from)
|
||||
if start_date:
|
||||
query = query.filter(
|
||||
DbWeighingSlip.gross_weight_time >= datetime.combine(start_date, datetime.min.time())
|
||||
)
|
||||
|
||||
if date_to:
|
||||
end_date = parse_date_str(date_to)
|
||||
if end_date:
|
||||
query = query.filter(DbWeighingSlip.gross_weight_time <= datetime_end_of_day(end_date))
|
||||
|
||||
if plan_no:
|
||||
query = query.filter(DbWeighingSlip.plan_no.contains(plan_no))
|
||||
|
||||
if batch_no:
|
||||
query = query.filter(DbWeighingSlip.batch_no.contains(batch_no))
|
||||
|
||||
if vehicle_no:
|
||||
query = query.filter(DbWeighingSlip.vehicle_no.contains(vehicle_no))
|
||||
|
||||
if cargo_name:
|
||||
query = query.filter(DbWeighingSlip.cargo_name.contains(cargo_name))
|
||||
|
||||
if supplier:
|
||||
query = query.filter(func.lower(DbWeighingSlip.supplier).contains(supplier.lower()))
|
||||
|
||||
if consignee:
|
||||
query = query.filter(func.lower(DbWeighingSlip.consignee).contains(consignee.lower()))
|
||||
|
||||
if business_type:
|
||||
query = query.filter(DbWeighingSlip.business_type == business_type)
|
||||
|
||||
weighings = query.all()
|
||||
inventory_ids = [w.inbound_outbound_id for w in weighings if w.inbound_outbound_id]
|
||||
inventory_date_map: dict[str, str] = {}
|
||||
if inventory_ids:
|
||||
inventories = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.id.in_(inventory_ids),
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for inventory in inventories:
|
||||
inventory_date_map[inventory.id] = inventory_date_to_api(inventory.date)
|
||||
|
||||
return [
|
||||
weighing_to_read(
|
||||
weighing,
|
||||
inventory_date=inventory_date_map.get(weighing.inbound_outbound_id or ""),
|
||||
)
|
||||
for weighing in weighings
|
||||
]
|
||||
|
||||
|
||||
@router.post("/", response_model=WeighingSlipRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_weighing(
|
||||
payload: WeighingSlipCreate,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> WeighingSlipRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
inbound_outbound_id = payload.inbound_outbound_id
|
||||
|
||||
if inbound_outbound_id:
|
||||
existing = (
|
||||
db.query(DbWeighingSlip)
|
||||
.filter(
|
||||
DbWeighingSlip.inbound_outbound_id == inbound_outbound_id,
|
||||
DbWeighingSlip.is_voided == 0,
|
||||
DbWeighingSlip.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="该出入库单已经存在关联的地磅单,无法重复创建",
|
||||
)
|
||||
|
||||
planned_quantity = payload.planned_quantity
|
||||
current_quantity = payload.current_quantity
|
||||
purchase_plan_id = payload.purchase_plan_id
|
||||
batch_no = payload.batch_no.strip() if payload.batch_no else None
|
||||
|
||||
if inbound_outbound_id:
|
||||
inventory = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.id == inbound_outbound_id,
|
||||
DbInboundOutbound.tenant_no == tenant_no,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if inventory:
|
||||
if current_quantity is None:
|
||||
current_quantity = inventory.quantity * 1000
|
||||
if purchase_plan_id is None and inventory.purchase_plan_id:
|
||||
purchase_plan_id = inventory.purchase_plan_id
|
||||
if purchase_plan_id:
|
||||
plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(DbPurchasePlan.id == purchase_plan_id)
|
||||
.one_or_none()
|
||||
)
|
||||
if plan and planned_quantity is None:
|
||||
planned_quantity = plan.planned_quantity * 1000
|
||||
if not batch_no and inventory.batch_no:
|
||||
batch_no = inventory.batch_no
|
||||
|
||||
gross_time = parse_datetime_str(payload.gross_weight_time) if payload.gross_weight_time else None
|
||||
tare_time = (
|
||||
parse_datetime_str(payload.tare_weight_time)
|
||||
if payload.tare_weight_time and payload.tare_weight_time.strip()
|
||||
else None
|
||||
)
|
||||
|
||||
snapshot = resolve_crm_product(db, tenant_no, payload.product_id)
|
||||
|
||||
row = DbWeighingSlip(
|
||||
id=generate_uuid(),
|
||||
tenant_no=tenant_no,
|
||||
vehicle_no=payload.vehicle_no.strip(),
|
||||
plan_no=payload.plan_no.strip() if payload.plan_no else None,
|
||||
business_type=payload.business_type,
|
||||
batch_no=batch_no,
|
||||
supplier=payload.supplier.strip(),
|
||||
consignee=payload.consignee.strip(),
|
||||
gross_weight=payload.gross_weight or 0,
|
||||
gross_weight_time=gross_time or datetime.now(),
|
||||
tare_weight=payload.tare_weight or 0,
|
||||
tare_weight_time=tare_time,
|
||||
deduction=payload.deduction or 0,
|
||||
net_weight=payload.net_weight or 0,
|
||||
planned_quantity=planned_quantity,
|
||||
executed_quantity=payload.executed_quantity,
|
||||
purchase_plan_id=purchase_plan_id,
|
||||
inbound_outbound_id=inbound_outbound_id,
|
||||
weighing_point=payload.weighing_point or "山峡化工",
|
||||
remark=payload.remarks,
|
||||
current_quantity=current_quantity,
|
||||
is_voided=0,
|
||||
create_by=user.id,
|
||||
create_dept_id=user.dept_id,
|
||||
)
|
||||
apply_product_snapshot_to_weighing(row, snapshot)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
|
||||
process_weighing_business_logic(db, row)
|
||||
db.refresh(row)
|
||||
return weighing_to_read(row)
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=WeighingSlipRead)
|
||||
def get_weighing(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> WeighingSlipRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_weighing(db, tenant_no, id)
|
||||
return weighing_to_read(row)
|
||||
|
||||
|
||||
@router.patch("/{id}", response_model=WeighingSlipRead)
|
||||
def update_weighing(
|
||||
id: str,
|
||||
payload: WeighingSlipUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> WeighingSlipRead:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_weighing(db, tenant_no, id)
|
||||
|
||||
if row.is_voided == 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot update voided weighing record",
|
||||
)
|
||||
|
||||
payload_dict = payload.model_dump(exclude_unset=True)
|
||||
_apply_weighing_product_update(db, tenant_no, row, payload_dict)
|
||||
_apply_weighing_update(row, payload_dict)
|
||||
row.update_by = user.id
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
|
||||
process_weighing_business_logic(db, row)
|
||||
db.refresh(row)
|
||||
return weighing_to_read(row)
|
||||
|
||||
|
||||
@router.post("/{id}/void", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def void_weighing(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> None:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_weighing(db, tenant_no, id)
|
||||
|
||||
if row.is_voided == 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Weighing record is already voided",
|
||||
)
|
||||
|
||||
row.is_voided = 1
|
||||
row.update_by = user.id
|
||||
db.commit()
|
||||
process_weighing_business_logic(db, row)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_weighing(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> None:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_weighing(db, tenant_no, id)
|
||||
row.is_voided = 1
|
||||
row.update_by = user.id
|
||||
db.commit()
|
||||
process_weighing_business_logic(db, row)
|
||||
|
||||
|
||||
class WeighingRelatedData(BaseModel):
|
||||
plan: Optional[PurchasePlanRead]
|
||||
inventory: Optional[InboundOutboundRead]
|
||||
|
||||
|
||||
@router.get("/{id}/related", response_model=WeighingRelatedData)
|
||||
def get_weighing_related(
|
||||
id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user: DbUserContext = Depends(get_db_current_user),
|
||||
) -> WeighingRelatedData:
|
||||
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
|
||||
row = _get_weighing(db, tenant_no, id)
|
||||
|
||||
plan = None
|
||||
if row.purchase_plan_id:
|
||||
plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(
|
||||
DbPurchasePlan.id == row.purchase_plan_id,
|
||||
DbPurchasePlan.del_flag == 0,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
elif row.plan_no:
|
||||
plan = (
|
||||
db.query(DbPurchasePlan)
|
||||
.filter(
|
||||
DbPurchasePlan.plan_no == row.plan_no,
|
||||
DbPurchasePlan.tenant_no == tenant_no,
|
||||
DbPurchasePlan.del_flag == 0,
|
||||
)
|
||||
.limit(1)
|
||||
.first()
|
||||
)
|
||||
|
||||
inventory = None
|
||||
if row.inbound_outbound_id:
|
||||
inventory = (
|
||||
db.query(DbInboundOutbound)
|
||||
.filter(
|
||||
DbInboundOutbound.id == row.inbound_outbound_id,
|
||||
DbInboundOutbound.del_flag == 0,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
return WeighingRelatedData(
|
||||
plan=plan_to_read(plan) if plan else None,
|
||||
inventory=inventory_to_read(inventory) if inventory else None,
|
||||
)
|
||||
Reference in new issue
Block a user