This commit is contained in:
zlwu committed 2026-06-10 22:59:34 +08:00
1 parent 725774ef0c
commit c3be9d1dce
176 files changed
+41925 -6342

No files matched your search

+2
View File
@@ -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",
]
+17
View File
@@ -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"}
+180
View File
@@ -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
]
+488
View File
@@ -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,
)
+475
View File
@@ -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],
)
+146
View File
@@ -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",
)
+414
View File
@@ -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,
)