| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- 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.delete_guard import assert_row_deletable_by_creator, user_info_from_context
- from app.core.tenant_context import get_tenant_no
- from app.core.utils import generate_uuid
- from app.services.db_data_scope import apply_db_scope, assert_db_row_in_scope
- 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, user: DbUserContext
- ) -> 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",
- )
- assert_db_row_in_scope(db, row, DbWeighingSlip, user, tenant_no)
- 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)
- query = apply_db_scope(query, DbWeighingSlip, user, db, tenant_no)
- 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, user)
- 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, user)
- 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, user)
- 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, user)
- assert_row_deletable_by_creator(row, user_info_from_context(user))
- 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, user)
- 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,
- )
|