868 lines
27 KiB
Python
868 lines
27 KiB
Python
"""运输计划业务逻辑"""
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.utils import generate_uuid
|
|
from app.models.pl_models import (
|
|
PlCarrier,
|
|
PlFreightSettlement,
|
|
PlFreightSettlementTransport,
|
|
PlPersonnel,
|
|
PlPlan,
|
|
PlPlanChange,
|
|
PlPlanProduct,
|
|
PlPlanTransport,
|
|
PlTransport,
|
|
PlVehicle,
|
|
PlVehiclePersonnel,
|
|
)
|
|
from app.schemas.pl.plan import (
|
|
PlanArchiveFreightBlock,
|
|
PlanArchiveProductItem,
|
|
PlanCreateFull,
|
|
PlanProductCreate,
|
|
PlanTransportCreateBlock,
|
|
)
|
|
|
|
|
|
def determine_plan_type(supplier_name: Optional[str], destination_name: Optional[str]) -> str:
|
|
if destination_name and ("安庆山峡" in destination_name or "安徽云栋" in destination_name):
|
|
return "inbound"
|
|
if supplier_name and ("安庆山峡" in supplier_name or "安徽云栋" in supplier_name):
|
|
return "outbound"
|
|
return "direct"
|
|
|
|
|
|
def is_ton_product_unit(unit: Optional[str]) -> bool:
|
|
code = (unit or "").strip()
|
|
if not code:
|
|
return True
|
|
return code in ("dun", "吨")
|
|
|
|
|
|
def _product_quantity(prod: PlanProductCreate) -> Decimal:
|
|
if prod.quantity is not None:
|
|
return Decimal(str(prod.quantity))
|
|
if prod.inbound_estimate_qty is not None:
|
|
return Decimal(str(prod.inbound_estimate_qty))
|
|
if prod.outbound_estimate_qty is not None:
|
|
return Decimal(str(prod.outbound_estimate_qty))
|
|
return Decimal("0")
|
|
|
|
|
|
def sum_product_quantities(products: list[PlanProductCreate]) -> Decimal:
|
|
return sum(_product_quantity(p) for p in products)
|
|
|
|
|
|
def _estimate_qty_fields(plan_type: str, qty: Optional[Decimal]) -> dict:
|
|
if qty is None:
|
|
return {}
|
|
q = float(qty)
|
|
if plan_type == "inbound":
|
|
return {"inbound_estimate_qty": q, "outbound_estimate_qty": q}
|
|
if plan_type == "outbound":
|
|
return {"outbound_estimate_qty": q, "inbound_estimate_qty": q}
|
|
return {"inbound_estimate_qty": q, "outbound_estimate_qty": q}
|
|
|
|
|
|
def build_plan_product(
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
plan_type: str,
|
|
prod: PlanProductCreate,
|
|
order_num: int,
|
|
user_id: str,
|
|
dept_id: str,
|
|
) -> PlPlanProduct:
|
|
qty = _product_quantity(prod)
|
|
estimates = _estimate_qty_fields(plan_type, qty if qty else None)
|
|
tax_price = prod.tax_price
|
|
return PlPlanProduct(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
plan_id=plan_id,
|
|
product_id=prod.product_id,
|
|
product_no=prod.product_no,
|
|
product_name=prod.product_name,
|
|
model=prod.model,
|
|
unit=prod.unit or "吨",
|
|
category_name=prod.category_name,
|
|
package_spec=prod.package_spec,
|
|
delivery_method=prod.delivery_method,
|
|
quantity=qty if qty else None,
|
|
actual_qty=Decimal(str(prod.actual_qty)) if prod.actual_qty is not None else (qty if qty else None),
|
|
tax_price=tax_price,
|
|
tax_amount=prod.tax_amount,
|
|
tax_subtotal=prod.tax_subtotal,
|
|
is_explosive=prod.is_explosive,
|
|
is_toxic=prod.is_toxic,
|
|
purchase_cert_url=prod.purchase_cert_url,
|
|
transport_cert_url=prod.transport_cert_url,
|
|
inbound_price=tax_price,
|
|
outbound_price=tax_price,
|
|
inbound_amount=prod.tax_subtotal,
|
|
outbound_amount=prod.tax_subtotal,
|
|
remark=prod.remark,
|
|
order_num=order_num,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
**estimates,
|
|
)
|
|
|
|
|
|
def _resolve_driver(
|
|
db: Session,
|
|
tenant_no: str,
|
|
vehicle_id: str,
|
|
driver_id: Optional[str],
|
|
) -> tuple[str, str, Optional[str]]:
|
|
if driver_id:
|
|
personnel = (
|
|
db.query(PlPersonnel)
|
|
.filter(
|
|
PlPersonnel.id == driver_id,
|
|
PlPersonnel.tenant_no == tenant_no,
|
|
PlPersonnel.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if personnel:
|
|
return personnel.id, personnel.personnel_name, personnel.phone_no
|
|
|
|
default_link = (
|
|
db.query(PlVehiclePersonnel)
|
|
.filter(
|
|
PlVehiclePersonnel.vehicle_id == vehicle_id,
|
|
PlVehiclePersonnel.tenant_no == tenant_no,
|
|
PlVehiclePersonnel.del_flag == 0,
|
|
PlVehiclePersonnel.role == "driver",
|
|
)
|
|
.order_by(PlVehiclePersonnel.is_default.desc(), PlVehiclePersonnel.order_num.asc())
|
|
.first()
|
|
)
|
|
if default_link:
|
|
personnel = (
|
|
db.query(PlPersonnel)
|
|
.filter(
|
|
PlPersonnel.id == default_link.personnel_id,
|
|
PlPersonnel.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if personnel:
|
|
return personnel.id, personnel.personnel_name, personnel.phone_no
|
|
|
|
raise HTTPException(status_code=400, detail="请选择司机或确保车辆已关联默认司机")
|
|
|
|
|
|
def _calc_freight_amount(
|
|
freight_type: str,
|
|
products: list[PlPlanProduct],
|
|
transport_qty: Optional[Decimal],
|
|
freight_price: Optional[Decimal],
|
|
freight_amount: Optional[Decimal],
|
|
) -> Decimal:
|
|
if freight_type == "by_trip":
|
|
return Decimal(str(freight_amount or 0))
|
|
|
|
qty = transport_qty
|
|
if qty is None:
|
|
qty = sum(
|
|
Decimal(str(p.quantity or p.inbound_estimate_qty or p.outbound_estimate_qty or 0))
|
|
for p in products
|
|
)
|
|
if freight_price is not None:
|
|
computed = qty * Decimal(str(freight_price))
|
|
if freight_amount is not None:
|
|
return Decimal(str(freight_amount))
|
|
return computed
|
|
return Decimal(str(freight_amount or 0))
|
|
|
|
|
|
def _validate_toxic_products(products: list[PlPlanProduct]) -> None:
|
|
for prod in products:
|
|
if prod.is_explosive or prod.is_toxic:
|
|
if not prod.purchase_cert_url or not prod.transport_cert_url:
|
|
label = "易制爆" if prod.is_explosive else "易制毒"
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"产品【{prod.product_name}】为{label},必须上传购买证和运输证",
|
|
)
|
|
|
|
|
|
def resolve_hazard_flags_from_crm(
|
|
db: Session,
|
|
tenant_no: str,
|
|
product_id: Optional[str],
|
|
is_toxic: int = 0,
|
|
is_explosive: int = 0,
|
|
) -> tuple[int, int]:
|
|
if is_toxic or is_explosive:
|
|
return int(is_toxic or 0), int(is_explosive or 0)
|
|
if not product_id:
|
|
return 0, 0
|
|
from app.models.crm_models import CrmProduct
|
|
|
|
row = (
|
|
db.query(CrmProduct)
|
|
.filter(
|
|
CrmProduct.id == product_id,
|
|
CrmProduct.tenant_no == tenant_no,
|
|
CrmProduct.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not row:
|
|
return 0, 0
|
|
return int(row.is_toxic or 0), int(row.is_explosive or 0)
|
|
|
|
|
|
def update_plan_product_certs(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
items: list,
|
|
user_id: str,
|
|
) -> list[PlPlanProduct]:
|
|
plan = (
|
|
db.query(PlPlan)
|
|
.filter(
|
|
PlPlan.id == plan_id,
|
|
PlPlan.tenant_no == tenant_no,
|
|
PlPlan.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not plan:
|
|
raise HTTPException(status_code=404, detail="计划不存在")
|
|
if plan.archive_status == "archived":
|
|
raise HTTPException(status_code=400, detail="已归档的计划不能修改")
|
|
|
|
updated: list[PlPlanProduct] = []
|
|
for item in items:
|
|
row = (
|
|
db.query(PlPlanProduct)
|
|
.filter(
|
|
PlPlanProduct.id == item.id,
|
|
PlPlanProduct.plan_id == plan_id,
|
|
PlPlanProduct.tenant_no == tenant_no,
|
|
PlPlanProduct.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail=f"计划产品不存在: {item.id}")
|
|
if not (row.is_toxic or row.is_explosive):
|
|
raise HTTPException(status_code=400, detail=f"产品【{row.product_name}】无需上传危化品证书")
|
|
row.purchase_cert_url = (item.purchase_cert_url or "").strip() or None
|
|
row.transport_cert_url = (item.transport_cert_url or "").strip() or None
|
|
row.update_by = user_id
|
|
updated.append(row)
|
|
|
|
plan.update_by = user_id
|
|
db.commit()
|
|
for row in updated:
|
|
db.refresh(row)
|
|
return updated
|
|
|
|
|
|
def _generate_transport_no(db: Session) -> str:
|
|
now = date.today()
|
|
prefix = f"SXYS{now.strftime('%Y%m')}"
|
|
last = (
|
|
db.query(PlTransport)
|
|
.filter(PlTransport.transport_no.like(f"{prefix}%"))
|
|
.order_by(PlTransport.transport_no.desc())
|
|
.first()
|
|
)
|
|
seq = 1
|
|
if last and last.transport_no:
|
|
try:
|
|
seq = int(last.transport_no[-6:]) + 1
|
|
except ValueError:
|
|
seq = 1
|
|
return f"{prefix}{seq:06d}"
|
|
|
|
|
|
def _resolve_transport_context(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
transport_payload: PlanTransportCreateBlock,
|
|
products: list[PlPlanProduct],
|
|
) -> tuple[PlVehicle, str, str, Optional[str], Optional[str], Optional[str], Decimal, Optional[Decimal]]:
|
|
vehicle = (
|
|
db.query(PlVehicle)
|
|
.filter(
|
|
PlVehicle.id == transport_payload.vehicle_id,
|
|
PlVehicle.tenant_no == tenant_no,
|
|
PlVehicle.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not vehicle:
|
|
raise HTTPException(status_code=400, detail="车辆不存在")
|
|
|
|
driver_id, driver_name, driver_phone = _resolve_driver(
|
|
db, tenant_no, transport_payload.vehicle_id, transport_payload.driver_id
|
|
)
|
|
carrier_id = transport_payload.carrier_id or vehicle.carrier_id
|
|
carrier_name = vehicle.carrier_name
|
|
if carrier_id:
|
|
carrier = (
|
|
db.query(PlCarrier)
|
|
.filter(PlCarrier.id == carrier_id, PlCarrier.del_flag == 0)
|
|
.first()
|
|
)
|
|
if carrier:
|
|
carrier_name = carrier.carrier_name
|
|
|
|
freight_amount = _calc_freight_amount(
|
|
transport_payload.freight_type,
|
|
products,
|
|
transport_payload.transport_qty,
|
|
transport_payload.freight_price,
|
|
transport_payload.freight_amount,
|
|
)
|
|
transport_qty = transport_payload.transport_qty
|
|
if transport_qty is None and transport_payload.freight_type == "by_weight":
|
|
transport_qty = sum(
|
|
Decimal(str(p.quantity or p.inbound_estimate_qty or p.outbound_estimate_qty or 0))
|
|
for p in products
|
|
)
|
|
|
|
return (
|
|
vehicle,
|
|
driver_id,
|
|
driver_name,
|
|
driver_phone,
|
|
carrier_id,
|
|
carrier_name,
|
|
freight_amount,
|
|
transport_qty,
|
|
)
|
|
|
|
|
|
def _apply_payload_to_transport_row(
|
|
transport: PlTransport,
|
|
*,
|
|
vehicle: PlVehicle,
|
|
driver_id: str,
|
|
driver_name: str,
|
|
driver_phone: Optional[str],
|
|
carrier_id: Optional[str],
|
|
carrier_name: Optional[str],
|
|
plan: PlPlan,
|
|
transport_payload: PlanTransportCreateBlock,
|
|
freight_amount: Decimal,
|
|
transport_qty: Optional[Decimal],
|
|
user_id: str,
|
|
) -> None:
|
|
transport.vehicle_id = vehicle.id
|
|
transport.license_plate = vehicle.license_plate
|
|
transport.driver_id = driver_id
|
|
transport.driver_name = driver_name
|
|
transport.driver_phone = driver_phone
|
|
transport.carrier_id = carrier_id
|
|
transport.carrier_name = carrier_name
|
|
transport.start_point = plan.pickup_address
|
|
transport.end_point = plan.destination_address or plan.destination_name
|
|
transport.distance_km = transport_payload.distance_km
|
|
transport.transport_qty = transport_qty
|
|
transport.freight_price = transport_payload.freight_price
|
|
transport.freight_amount = freight_amount or 0
|
|
transport.freight_type = transport_payload.freight_type
|
|
transport.has_invoice = transport_payload.has_invoice
|
|
transport.unit = "吨" if transport_payload.freight_type == "by_weight" else "趟"
|
|
if transport_payload.remark is not None:
|
|
transport.remark = transport_payload.remark
|
|
transport.update_by = user_id
|
|
|
|
|
|
def upsert_draft_transport_for_plan(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
transport_payload: PlanTransportCreateBlock,
|
|
products: list[PlPlanProduct],
|
|
user_id: str,
|
|
dept_id: str,
|
|
) -> None:
|
|
"""写入/更新草稿运输单(pl_transport + pl_plan_transport),计划保持待安排。"""
|
|
(
|
|
vehicle,
|
|
driver_id,
|
|
driver_name,
|
|
driver_phone,
|
|
carrier_id,
|
|
carrier_name,
|
|
freight_amount,
|
|
transport_qty,
|
|
) = _resolve_transport_context(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan=plan,
|
|
transport_payload=transport_payload,
|
|
products=products,
|
|
)
|
|
|
|
transport: Optional[PlTransport] = None
|
|
if plan.transport_id:
|
|
transport = (
|
|
db.query(PlTransport)
|
|
.filter(
|
|
PlTransport.id == plan.transport_id,
|
|
PlTransport.tenant_no == tenant_no,
|
|
PlTransport.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if transport:
|
|
_apply_payload_to_transport_row(
|
|
transport,
|
|
vehicle=vehicle,
|
|
driver_id=driver_id,
|
|
driver_name=driver_name,
|
|
driver_phone=driver_phone,
|
|
carrier_id=carrier_id,
|
|
carrier_name=carrier_name,
|
|
plan=plan,
|
|
transport_payload=transport_payload,
|
|
freight_amount=freight_amount,
|
|
transport_qty=transport_qty,
|
|
user_id=user_id,
|
|
)
|
|
if transport.transport_status == "draft":
|
|
pass
|
|
else:
|
|
transport_no = _generate_transport_no(db)
|
|
transport = PlTransport(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
transport_no=transport_no,
|
|
transport_status="draft",
|
|
approval_status="none",
|
|
remark=transport_payload.remark,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
)
|
|
db.add(transport)
|
|
db.flush()
|
|
db.add(
|
|
PlPlanTransport(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
plan_id=plan.id,
|
|
transport_id=transport.id,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
)
|
|
)
|
|
plan.transport_id = transport.id
|
|
plan.transport_no = transport_no
|
|
_apply_payload_to_transport_row(
|
|
transport,
|
|
vehicle=vehicle,
|
|
driver_id=driver_id,
|
|
driver_name=driver_name,
|
|
driver_phone=driver_phone,
|
|
carrier_id=carrier_id,
|
|
carrier_name=carrier_name,
|
|
plan=plan,
|
|
transport_payload=transport_payload,
|
|
freight_amount=freight_amount,
|
|
transport_qty=transport_qty,
|
|
user_id=user_id,
|
|
)
|
|
|
|
plan.vehicle_id = vehicle.id
|
|
plan.license_plate = vehicle.license_plate
|
|
plan.driver_id = driver_id
|
|
plan.driver_name = driver_name
|
|
plan.freight_amount = freight_amount or 0
|
|
if plan.transport_status != "arranged":
|
|
plan.transport_status = "pending"
|
|
|
|
|
|
def clear_draft_transport_for_plan(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
user_id: str,
|
|
) -> None:
|
|
"""清除计划上的草稿运输单及车辆预选。"""
|
|
if plan.transport_id:
|
|
transport = (
|
|
db.query(PlTransport)
|
|
.filter(
|
|
PlTransport.id == plan.transport_id,
|
|
PlTransport.tenant_no == tenant_no,
|
|
PlTransport.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if transport and transport.transport_status == "draft":
|
|
transport.del_flag = 1
|
|
transport.update_by = user_id
|
|
db.query(PlPlanTransport).filter(
|
|
PlPlanTransport.plan_id == plan.id,
|
|
PlPlanTransport.transport_id == transport.id,
|
|
PlPlanTransport.tenant_no == tenant_no,
|
|
PlPlanTransport.del_flag == 0,
|
|
).update({"del_flag": 1, "update_by": user_id})
|
|
plan.transport_id = None
|
|
plan.transport_no = None
|
|
|
|
plan.vehicle_id = None
|
|
plan.license_plate = None
|
|
plan.driver_id = None
|
|
plan.driver_name = None
|
|
plan.freight_amount = 0
|
|
if plan.transport_status != "arranged":
|
|
plan.transport_status = "pending"
|
|
|
|
|
|
def sync_transport_prefill_for_plan(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
transport_payload: PlanTransportCreateBlock,
|
|
products: list[PlPlanProduct],
|
|
user_id: str,
|
|
dept_id: str = "",
|
|
) -> None:
|
|
"""同步运输预选到草稿运输子表(pl_transport)。"""
|
|
upsert_draft_transport_for_plan(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan=plan,
|
|
transport_payload=transport_payload,
|
|
products=products,
|
|
user_id=user_id,
|
|
dept_id=dept_id or user_id,
|
|
)
|
|
|
|
|
|
def apply_vehicle_prefill_to_plan(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
transport_payload: PlanTransportCreateBlock,
|
|
products: list[PlPlanProduct],
|
|
user_id: str = "",
|
|
dept_id: str = "",
|
|
) -> None:
|
|
"""兼容旧调用:写入草稿运输子表。"""
|
|
upsert_draft_transport_for_plan(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan=plan,
|
|
transport_payload=transport_payload,
|
|
products=products,
|
|
user_id=user_id,
|
|
dept_id=dept_id or user_id,
|
|
)
|
|
|
|
|
|
def create_transport_for_plan(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
products: list[PlPlanProduct],
|
|
transport_payload: PlanTransportCreateBlock,
|
|
user_id: str,
|
|
dept_id: str,
|
|
generate_transport_no,
|
|
) -> PlTransport:
|
|
vehicle = (
|
|
db.query(PlVehicle)
|
|
.filter(
|
|
PlVehicle.id == transport_payload.vehicle_id,
|
|
PlVehicle.tenant_no == tenant_no,
|
|
PlVehicle.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not vehicle:
|
|
raise HTTPException(status_code=400, detail="车辆不存在")
|
|
|
|
_validate_toxic_products(products)
|
|
|
|
driver_id, driver_name, driver_phone = _resolve_driver(
|
|
db, tenant_no, transport_payload.vehicle_id, transport_payload.driver_id
|
|
)
|
|
|
|
carrier_name = vehicle.carrier_name
|
|
carrier_id = transport_payload.carrier_id or vehicle.carrier_id
|
|
if carrier_id:
|
|
carrier = (
|
|
db.query(PlCarrier)
|
|
.filter(PlCarrier.id == carrier_id, PlCarrier.del_flag == 0)
|
|
.first()
|
|
)
|
|
if carrier:
|
|
carrier_name = carrier.carrier_name
|
|
|
|
freight_amount = _calc_freight_amount(
|
|
transport_payload.freight_type,
|
|
products,
|
|
transport_payload.transport_qty,
|
|
transport_payload.freight_price,
|
|
transport_payload.freight_amount,
|
|
)
|
|
|
|
transport_qty = transport_payload.transport_qty
|
|
if transport_qty is None and transport_payload.freight_type == "by_weight":
|
|
transport_qty = sum(
|
|
Decimal(str(p.quantity or p.inbound_estimate_qty or p.outbound_estimate_qty or 0))
|
|
for p in products
|
|
)
|
|
|
|
transport_no = generate_transport_no(db)
|
|
transport = PlTransport(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
transport_no=transport_no,
|
|
vehicle_id=vehicle.id,
|
|
license_plate=vehicle.license_plate,
|
|
driver_id=driver_id,
|
|
driver_name=driver_name,
|
|
driver_phone=driver_phone,
|
|
carrier_id=carrier_id,
|
|
carrier_name=carrier_name,
|
|
start_point=plan.pickup_address,
|
|
end_point=plan.destination_address or plan.destination_name,
|
|
distance_km=transport_payload.distance_km,
|
|
unit="吨" if transport_payload.freight_type == "by_weight" else "趟",
|
|
transport_qty=transport_qty,
|
|
freight_price=transport_payload.freight_price,
|
|
freight_amount=freight_amount or 0,
|
|
freight_type=transport_payload.freight_type,
|
|
has_invoice=transport_payload.has_invoice,
|
|
transport_status="draft",
|
|
approval_status="none",
|
|
remark=transport_payload.remark,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
)
|
|
db.add(transport)
|
|
|
|
db.add(
|
|
PlPlanTransport(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
plan_id=plan.id,
|
|
transport_id=transport.id,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
)
|
|
)
|
|
|
|
plan.transport_id = transport.id
|
|
plan.transport_no = transport_no
|
|
plan.vehicle_id = vehicle.id
|
|
plan.license_plate = vehicle.license_plate
|
|
plan.driver_id = driver_id
|
|
plan.driver_name = driver_name
|
|
plan.transport_status = "arranged"
|
|
plan.freight_amount = freight_amount or 0
|
|
|
|
return transport
|
|
|
|
|
|
def validate_create_full(payload: PlanCreateFull) -> None:
|
|
if not payload.contract_id:
|
|
raise HTTPException(status_code=422, detail="请选择合同")
|
|
if not payload.products:
|
|
raise HTTPException(status_code=422, detail="请至少保留一条产品明细")
|
|
|
|
transport = payload.transport
|
|
if not transport or not transport.vehicle_id:
|
|
return
|
|
|
|
if payload.has_freight:
|
|
if transport.freight_type == "by_weight":
|
|
if not transport.freight_price or Decimal(str(transport.freight_price)) <= 0:
|
|
raise HTTPException(status_code=422, detail="按重量计费时请填写运输单价")
|
|
qty = transport.transport_qty or sum_product_quantities(payload.products)
|
|
if qty <= 0:
|
|
raise HTTPException(status_code=422, detail="运输量必须大于0")
|
|
elif transport.freight_type == "by_trip":
|
|
if not transport.freight_amount or Decimal(str(transport.freight_amount)) <= 0:
|
|
raise HTTPException(status_code=422, detail="按趟计费时请填写运费金额")
|
|
|
|
|
|
def calc_plan_total_amount(products: list[PlanProductCreate]) -> Decimal:
|
|
total = Decimal("0")
|
|
for prod in products:
|
|
if prod.tax_subtotal is not None:
|
|
total += Decimal(str(prod.tax_subtotal))
|
|
return total
|
|
|
|
|
|
_LOCKED_SETTLEMENT_STATUSES = ("approving", "approved", "confirmed")
|
|
|
|
|
|
def transport_has_locked_freight_settlement(
|
|
db: Session, tenant_no: str, transport_id: Optional[str]
|
|
) -> bool:
|
|
if not transport_id:
|
|
return False
|
|
return (
|
|
db.query(PlFreightSettlementTransport.id)
|
|
.join(
|
|
PlFreightSettlement,
|
|
PlFreightSettlement.id == PlFreightSettlementTransport.settlement_id,
|
|
)
|
|
.filter(
|
|
PlFreightSettlementTransport.transport_id == transport_id,
|
|
PlFreightSettlementTransport.tenant_no == tenant_no,
|
|
PlFreightSettlementTransport.del_flag == 0,
|
|
PlFreightSettlement.tenant_no == tenant_no,
|
|
PlFreightSettlement.del_flag == 0,
|
|
PlFreightSettlement.settlement_status.in_(_LOCKED_SETTLEMENT_STATUSES),
|
|
)
|
|
.first()
|
|
is not None
|
|
)
|
|
|
|
|
|
def apply_archive_product_quantities(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
items: list[PlanArchiveProductItem],
|
|
user_id: str,
|
|
) -> None:
|
|
product_map = {
|
|
row.id: row
|
|
for row in db.query(PlPlanProduct).filter(
|
|
PlPlanProduct.plan_id == plan.id,
|
|
PlPlanProduct.tenant_no == tenant_no,
|
|
PlPlanProduct.del_flag == 0,
|
|
).all()
|
|
}
|
|
for item in items:
|
|
prod = product_map.get(item.id)
|
|
if not prod:
|
|
raise HTTPException(status_code=400, detail=f"产品明细不存在: {item.id}")
|
|
actual = Decimal(str(item.actual_qty))
|
|
prod.actual_qty = actual
|
|
price_in = prod.inbound_price or prod.tax_price
|
|
price_out = prod.outbound_price or prod.tax_price
|
|
if plan.plan_type == "inbound":
|
|
prod.inbound_qty = actual
|
|
if price_in is not None:
|
|
prod.inbound_amount = actual * Decimal(str(price_in))
|
|
elif plan.plan_type == "outbound":
|
|
prod.outbound_qty = actual
|
|
if price_out is not None:
|
|
prod.outbound_amount = actual * Decimal(str(price_out))
|
|
prod.update_by = user_id
|
|
|
|
|
|
def apply_archive_freight(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
freight: PlanArchiveFreightBlock,
|
|
user_id: str,
|
|
) -> None:
|
|
has_freight = int(freight.has_freight if freight.has_freight is not None else plan.has_freight or 0)
|
|
plan.has_freight = has_freight
|
|
plan.update_by = user_id
|
|
|
|
if has_freight != 1:
|
|
plan.freight_amount = Decimal("0")
|
|
if not plan.transport_id:
|
|
return
|
|
if transport_has_locked_freight_settlement(db, tenant_no, plan.transport_id):
|
|
raise HTTPException(status_code=400, detail="运输单已纳入运费结算,无法修改运费")
|
|
transport = (
|
|
db.query(PlTransport)
|
|
.filter(
|
|
PlTransport.id == plan.transport_id,
|
|
PlTransport.tenant_no == tenant_no,
|
|
PlTransport.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not transport:
|
|
raise HTTPException(status_code=400, detail="关联运输单不存在")
|
|
transport.freight_amount = Decimal("0")
|
|
transport.freight_price = None
|
|
transport.transport_qty = None
|
|
transport.update_by = user_id
|
|
return
|
|
|
|
if not plan.transport_id:
|
|
raise HTTPException(status_code=400, detail="计划有运输费用但未安排运输,请先安排运输")
|
|
if transport_has_locked_freight_settlement(db, tenant_no, plan.transport_id):
|
|
raise HTTPException(status_code=400, detail="运输单已纳入运费结算,无法修改运费")
|
|
|
|
transport = (
|
|
db.query(PlTransport)
|
|
.filter(
|
|
PlTransport.id == plan.transport_id,
|
|
PlTransport.tenant_no == tenant_no,
|
|
PlTransport.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not transport:
|
|
raise HTTPException(status_code=400, detail="关联运输单不存在")
|
|
|
|
transport.distance_km = freight.distance_km
|
|
transport.freight_type = freight.freight_type or "by_weight"
|
|
transport.freight_price = freight.freight_price
|
|
amount = freight.freight_amount or Decimal("0")
|
|
transport.freight_amount = amount
|
|
if freight.transport_qty is not None:
|
|
transport.transport_qty = freight.transport_qty
|
|
if freight.has_invoice is not None:
|
|
transport.has_invoice = freight.has_invoice
|
|
transport.update_by = user_id
|
|
|
|
plan.freight_amount = amount
|
|
|
|
|
|
def record_plan_change(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
change_type: str,
|
|
change_content: str,
|
|
user_id: str,
|
|
user_name: Optional[str],
|
|
dept_id: str,
|
|
) -> None:
|
|
db.add(
|
|
PlPlanChange(
|
|
id=generate_uuid(),
|
|
tenant_no=tenant_no,
|
|
plan_id=plan_id,
|
|
change_type=change_type,
|
|
change_content=change_content,
|
|
change_user_id=user_id,
|
|
change_user_name=user_name,
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
)
|
|
)
|