Files
ShiErFeng/shierfeng-fastapi-backend/module_business/dao/shipment_summary_dao.py
T
yujiangandClaude Opus 5.5 3050ccdc4e Feat: 后端新增基础资料与业务管理只读汇总接口
为 Web 端改版提供名称、统计和看板数据。只新增只读查询、扩展列表与详情
返回字段;写接口、托盘状态机和幂等键规则均未改动,未新增权限标识。

- 基础资料:列表补充名称与汇总,客户详情返回现场树,托盘详情返回最近流转,
  托盘列表支持按流转阶段筛选和二维码精确匹配;新增现场、产线、隧道的上级
  资料选项接口;Excel 导入支持 preview 只校验不写入
- 发货出库:列表支持 keyword、receiptState 筛选并补充名称、托盘数、包数和
  菌种构成;详情明细补充托盘号、菌种、批次
- 首次入库:列表与详情补充名称、托盘数、托盘当前状态;新增 /summary
- 托盘退回:列表支持 keyword 并补充名称与托盘汇总,详情补充托盘号和隧道;
  新增 /summary、/site-options
- 工厂装填:列表支持 keyword 并补充托盘号、仓库、操作人;新增 /summary、
  /batches、/batches/detail
- 客户现场使用:列表支持 keyword、fifoSkipped 并补充名称和被跳过的先到托盘号;
  新增 /site-options、/site-board 以及 usage-fifo 的 /queue
- 先到先用 DAO 抽出公共候选条件与排序,建议查询生成的 SQL 与改动前一致
- 新增各汇总 DAO 的单元测试,同步更新路由权限与模型字段契约测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 05:03:10 -07:00

168 lines
6.7 KiB
Python

from typing import Any
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased
from module_admin.entity.do.user_do import SysUser
from module_business.domain.enums import DocumentStatus, PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
from module_business.entity.do.master_do import BizCustomer, BizCustomerSite, BizWarehouse
from module_business.entity.do.operation_do import (
BizFillingRecord,
BizPalletInventory,
BizShipmentItem,
BizShipmentOrder,
)
class ShipmentSummaryDao:
"""发货单列表的只读汇总:名称、托盘数、包数与菌种构成。
只汇总调用方已按数据范围分页得到的单据 ID,所有关联表均受同一测试批次范围约束。
"""
@staticmethod
async def get_list_summaries(
db: AsyncSession,
shipment_ids: list[int],
scope: TestBatchScope,
) -> dict[int, dict[str, Any]]:
ids = sorted(set(shipment_ids))
if not ids:
return {}
source_warehouse = aliased(BizWarehouse, name='source_warehouse')
target_site = aliased(BizCustomerSite, name='target_site')
target_customer = aliased(BizCustomer, name='target_customer')
creator = aliased(SysUser, name='creator')
name_statement = (
select(
BizShipmentOrder.shipment_id,
source_warehouse.warehouse_name,
target_site.site_name,
target_customer.customer_name,
creator.nick_name,
)
.select_from(BizShipmentOrder)
.outerjoin(
source_warehouse,
and_(
source_warehouse.warehouse_id == BizShipmentOrder.source_warehouse_id,
batch_scope_condition(source_warehouse.test_batch_id, scope),
),
)
.outerjoin(
target_site,
and_(
target_site.site_id == BizShipmentOrder.target_site_id,
batch_scope_condition(target_site.test_batch_id, scope),
),
)
.outerjoin(
target_customer,
and_(
target_customer.customer_id == target_site.customer_id,
batch_scope_condition(target_customer.test_batch_id, scope),
),
)
.outerjoin(creator, and_(creator.user_name == BizShipmentOrder.create_by, creator.del_flag == '0'))
.where(
BizShipmentOrder.shipment_id.in_(ids),
batch_scope_condition(BizShipmentOrder.test_batch_id, scope),
)
)
# 已确认单使用确认快照;草稿与详情一致,只取来源仓库内仍可发货的当前余量
draft_inventory = and_(
BizShipmentOrder.status == DocumentStatus.DRAFT.value,
BizPalletInventory.pallet_id == BizShipmentItem.pallet_id,
BizPalletInventory.warehouse_id == BizShipmentOrder.source_warehouse_id,
BizPalletInventory.flow_state == PalletFlowState.FILLED_IN_WAREHOUSE.value,
BizPalletInventory.site_id.is_(None),
BizPalletInventory.current_line_id.is_(None),
BizPalletInventory.active_filling_id.is_not(None),
BizPalletInventory.remaining_packages > 0,
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
item_statement = (
select(
BizShipmentItem.shipment_id,
BizShipmentItem.package_qty,
BizPalletInventory.remaining_packages,
BizFillingRecord.strain_code_snapshot,
BizFillingRecord.strain_name_snapshot,
)
.select_from(BizShipmentItem)
.join(
BizShipmentOrder,
and_(
BizShipmentOrder.shipment_id == BizShipmentItem.shipment_id,
batch_scope_condition(BizShipmentOrder.test_batch_id, scope),
),
)
.outerjoin(BizPalletInventory, draft_inventory)
.outerjoin(
BizFillingRecord,
and_(
BizFillingRecord.filling_id
== func.coalesce(BizShipmentItem.active_filling_id, BizPalletInventory.active_filling_id),
BizFillingRecord.pallet_id == BizShipmentItem.pallet_id,
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
),
)
.where(
BizShipmentItem.shipment_id.in_(ids),
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.shipment_id, BizShipmentItem.shipment_item_id)
)
summaries: dict[int, dict[str, Any]] = {
shipment_id: {
'source_warehouse_name': None,
'target_site_name': None,
'customer_name': None,
'creator_name': None,
'pallet_count': 0,
'package_qty': None,
'strains': [],
}
for shipment_id in ids
}
for shipment_id, warehouse_name, site_name, customer_name, creator_name in (
await db.execute(name_statement)
).all():
summary = summaries.get(shipment_id)
if summary is None:
continue
summary.update(
source_warehouse_name=warehouse_name,
target_site_name=site_name,
customer_name=customer_name,
creator_name=creator_name,
)
strain_counts: dict[int, dict[str, int]] = {}
for shipment_id, package_qty, remaining_packages, strain_code, strain_name in (
await db.execute(item_statement)
).all():
summary = summaries.get(shipment_id)
if summary is None:
continue
summary['pallet_count'] += 1
quantity = package_qty if package_qty is not None else remaining_packages
if quantity is not None:
summary['package_qty'] = (summary['package_qty'] or 0) + quantity
label = strain_name or strain_code
if label:
counts = strain_counts.setdefault(shipment_id, {})
counts[label] = counts.get(label, 0) + 1
for shipment_id, counts in strain_counts.items():
summaries[shipment_id]['strains'] = [
{'strain_name': name, 'pallet_count': count}
for name, count in sorted(counts.items(), key=lambda entry: (-entry[1], entry[0]))
]
return summaries