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>
This commit is contained in:
2026-10-18 05:03:10 -07:00
co-authored by Claude Opus 5.5
parent 80a71b48ce
commit 3050ccdc4e
51 changed files with 6545 additions and 103 deletions
@@ -3,12 +3,13 @@ from typing import Any
from sqlalchemy import ColumnElement, Select, and_, delete, false, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased
from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
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 BizPallet
from module_business.entity.do.master_do import BizCustomer, BizCustomerSite, BizPallet, BizWarehouse
from module_business.entity.do.operation_do import (
BizFillingRecord,
BizInboundItem,
@@ -608,6 +609,15 @@ class OperationDao:
conditions.append(BizFillingRecord.batch_no.like(f'%{query_object.batch_no}%'))
if query_object.pallet_code:
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
if query_object.keyword:
keyword = f'%{query_object.keyword}%'
conditions.append(
or_(
BizFillingRecord.filling_no.like(keyword),
BizPallet.pallet_code.like(keyword),
BizFillingRecord.batch_no.like(keyword),
)
)
conditions.extend(
cls._time_conditions(
BizFillingRecord.operated_at,
@@ -616,7 +626,7 @@ class OperationDao:
)
)
statement = select(BizFillingRecord)
if query_object.pallet_code:
if query_object.pallet_code or query_object.keyword:
statement = statement.join(
BizPallet,
BizPallet.pallet_id == BizFillingRecord.pallet_id,
@@ -680,6 +690,10 @@ class OperationDao:
conditions.append(BizShipmentOrder.target_site_id == query_object.target_site_id)
if query_object.status:
conditions.append(BizShipmentOrder.status == query_object.status.value)
if query_object.receipt_state:
# 收货状态只在确认后有意义,草稿的默认值不参与筛选
conditions.append(BizShipmentOrder.status == DocumentStatus.CONFIRMED.value)
conditions.append(BizShipmentOrder.receipt_state == query_object.receipt_state)
conditions.extend(
cls._time_conditions(
BizShipmentOrder.create_time,
@@ -687,7 +701,45 @@ class OperationDao:
query_object.end_time,
)
)
statement = select(BizShipmentOrder).where(*conditions).order_by(BizShipmentOrder.shipment_id)
statement = select(BizShipmentOrder)
if query_object.keyword:
keyword = f'%{query_object.keyword}%'
source_warehouse = aliased(BizWarehouse, name='source_warehouse')
target_site = aliased(BizCustomerSite, name='target_site')
target_customer = aliased(BizCustomer, name='target_customer')
# 名称关联与单据处于同一测试批次范围,跨批次的主数据不会被匹配
statement = (
statement.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),
),
)
)
conditions.append(
or_(
BizShipmentOrder.shipment_no.like(keyword),
source_warehouse.warehouse_name.like(keyword),
target_site.site_name.like(keyword),
target_customer.customer_name.like(keyword),
)
)
statement = statement.where(*conditions).order_by(BizShipmentOrder.shipment_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
@@ -818,6 +870,11 @@ class OperationDao:
conditions.append(BizUsageRecord.line_id == query_object.line_id)
if query_object.pallet_code:
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
if query_object.keyword:
keyword = f'%{query_object.keyword}%'
conditions.append(or_(BizUsageRecord.usage_no.like(keyword), BizPallet.pallet_code.like(keyword)))
if query_object.fifo_skipped:
conditions.append(BizUsageRecord.fifo_skip_reason.is_not(None))
conditions.extend(
cls._time_conditions(
BizUsageRecord.occurred_at,
@@ -826,7 +883,7 @@ class OperationDao:
)
)
statement = select(BizUsageRecord)
if query_object.pallet_code:
if query_object.pallet_code or query_object.keyword:
statement = statement.join(
BizPallet,
BizPallet.pallet_id == BizUsageRecord.pallet_id,
@@ -884,7 +941,63 @@ class OperationDao:
query_object.end_time,
)
)
statement = select(BizReturnOrder).where(*conditions).order_by(BizReturnOrder.return_id)
statement = select(BizReturnOrder)
if query_object.keyword:
keyword = f'%{query_object.keyword}%'
source_site = aliased(BizCustomerSite, name='source_site')
source_customer = aliased(BizCustomer, name='source_customer')
target_warehouse = aliased(BizWarehouse, name='target_warehouse')
# 名称关联与单据处于同一测试批次范围,跨批次的主数据不会被匹配
statement = (
statement.outerjoin(
source_site,
and_(
source_site.site_id == BizReturnOrder.source_site_id,
batch_scope_condition(source_site.test_batch_id, scope),
),
)
.outerjoin(
source_customer,
and_(
source_customer.customer_id == source_site.customer_id,
batch_scope_condition(source_customer.test_batch_id, scope),
),
)
.outerjoin(
target_warehouse,
and_(
target_warehouse.warehouse_id == BizReturnOrder.target_warehouse_id,
batch_scope_condition(target_warehouse.test_batch_id, scope),
),
)
)
# 托盘编号按明细存在性匹配,不改变每张单据只出现一行
pallet_match = (
select(BizReturnItem.return_item_id)
.join(
BizPallet,
and_(
BizPallet.pallet_id == BizReturnItem.pallet_id,
batch_scope_condition(BizPallet.test_batch_id, scope),
),
)
.where(
BizReturnItem.return_id == BizReturnOrder.return_id,
batch_scope_condition(BizReturnItem.test_batch_id, scope),
BizPallet.pallet_code.like(keyword),
)
.exists()
)
conditions.append(
or_(
BizReturnOrder.return_no.like(keyword),
source_site.site_name.like(keyword),
source_customer.customer_name.like(keyword),
target_warehouse.warehouse_name.like(keyword),
pallet_match,
)
)
statement = statement.where(*conditions).order_by(BizReturnOrder.return_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod