415 lines
14 KiB
Python
415 lines
14 KiB
Python
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,
|
|
)
|