为 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>
168 lines
6.7 KiB
Python
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
|