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:
@@ -0,0 +1,220 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import and_, false, 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 BizPallet, BizWarehouse
|
||||
from module_business.entity.do.operation_do import BizInboundItem, BizInboundOrder, BizPalletInventory
|
||||
|
||||
|
||||
class InboundSummaryDao:
|
||||
"""首次入库的只读汇总:列表名称与托盘数、详情托盘编号与当前状态、页面概况。
|
||||
|
||||
列表与详情只汇总调用方已按数据范围取得的单据,所有关联表均受同一测试批次范围约束。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def get_list_summaries(
|
||||
db: AsyncSession,
|
||||
inbound_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = sorted(set(inbound_ids))
|
||||
if not ids:
|
||||
return {}
|
||||
|
||||
creator = aliased(SysUser, name='creator')
|
||||
confirmer = aliased(SysUser, name='confirmer')
|
||||
name_statement = (
|
||||
select(
|
||||
BizInboundOrder.inbound_id,
|
||||
BizWarehouse.warehouse_name,
|
||||
creator.nick_name,
|
||||
confirmer.nick_name,
|
||||
)
|
||||
.select_from(BizInboundOrder)
|
||||
.outerjoin(
|
||||
BizWarehouse,
|
||||
and_(
|
||||
BizWarehouse.warehouse_id == BizInboundOrder.warehouse_id,
|
||||
batch_scope_condition(BizWarehouse.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(creator, and_(creator.user_name == BizInboundOrder.create_by, creator.del_flag == '0'))
|
||||
.outerjoin(confirmer, and_(confirmer.user_id == BizInboundOrder.confirmed_by, confirmer.del_flag == '0'))
|
||||
.where(
|
||||
BizInboundOrder.inbound_id.in_(ids),
|
||||
batch_scope_condition(BizInboundOrder.test_batch_id, scope),
|
||||
)
|
||||
)
|
||||
count_statement = (
|
||||
select(BizInboundItem.inbound_id, func.count(BizInboundItem.inbound_item_id))
|
||||
.where(
|
||||
BizInboundItem.inbound_id.in_(ids),
|
||||
batch_scope_condition(BizInboundItem.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizInboundItem.inbound_id)
|
||||
)
|
||||
|
||||
summaries: dict[int, dict[str, Any]] = {
|
||||
inbound_id: {
|
||||
'warehouse_name': None,
|
||||
'creator_name': None,
|
||||
'confirmer_name': None,
|
||||
'pallet_count': 0,
|
||||
}
|
||||
for inbound_id in ids
|
||||
}
|
||||
for inbound_id, warehouse_name, creator_name, confirmer_name in (await db.execute(name_statement)).all():
|
||||
summary = summaries.get(inbound_id)
|
||||
if summary is None:
|
||||
continue
|
||||
summary.update(
|
||||
warehouse_name=warehouse_name,
|
||||
creator_name=creator_name,
|
||||
confirmer_name=confirmer_name,
|
||||
)
|
||||
for inbound_id, pallet_count in (await db.execute(count_statement)).all():
|
||||
summary = summaries.get(inbound_id)
|
||||
if summary is not None:
|
||||
summary['pallet_count'] = int(pallet_count or 0)
|
||||
return summaries
|
||||
|
||||
@staticmethod
|
||||
async def get_item_identities(
|
||||
db: AsyncSession,
|
||||
inbound_id: int,
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
"""为详情明细补充托盘编号与当前流转状态,只读展示,不参与任何写入。"""
|
||||
statement = (
|
||||
select(BizInboundItem.inbound_item_id, BizPallet.pallet_code, BizPalletInventory.flow_state)
|
||||
.select_from(BizInboundItem)
|
||||
.outerjoin(
|
||||
BizPallet,
|
||||
and_(
|
||||
BizPallet.pallet_id == BizInboundItem.pallet_id,
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
BizPalletInventory,
|
||||
and_(
|
||||
BizPalletInventory.pallet_id == BizInboundItem.pallet_id,
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
BizInboundItem.inbound_id == inbound_id,
|
||||
batch_scope_condition(BizInboundItem.test_batch_id, scope),
|
||||
)
|
||||
)
|
||||
return {
|
||||
inbound_item_id: {'pallet_code': pallet_code, 'flow_state': flow_state}
|
||||
for inbound_item_id, pallet_code, flow_state in (await db.execute(statement)).all()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_overview(
|
||||
db: AsyncSession,
|
||||
warehouse_ids: list[int] | None,
|
||||
scope: TestBatchScope,
|
||||
month_start: datetime,
|
||||
) -> dict[str, Any]:
|
||||
"""入库页概况。单据与空托盘按仓库数据范围统计;未入库托盘尚无仓库归属,按测试批次范围统计。"""
|
||||
order_conditions = [batch_scope_condition(BizInboundOrder.test_batch_id, scope)]
|
||||
inventory_conditions = [batch_scope_condition(BizPalletInventory.test_batch_id, scope)]
|
||||
if warehouse_ids is not None:
|
||||
scoped_ids = sorted(set(warehouse_ids))
|
||||
order_conditions.append(BizInboundOrder.warehouse_id.in_(scoped_ids) if scoped_ids else false())
|
||||
inventory_conditions.append(BizPalletInventory.warehouse_id.in_(scoped_ids) if scoped_ids else false())
|
||||
|
||||
item_join = and_(
|
||||
BizInboundItem.inbound_id == BizInboundOrder.inbound_id,
|
||||
batch_scope_condition(BizInboundItem.test_batch_id, scope),
|
||||
)
|
||||
draft_statement = (
|
||||
select(
|
||||
func.count(func.distinct(BizInboundOrder.inbound_id)),
|
||||
func.count(BizInboundItem.inbound_item_id),
|
||||
func.min(BizInboundOrder.create_time),
|
||||
)
|
||||
.select_from(BizInboundOrder)
|
||||
.outerjoin(BizInboundItem, item_join)
|
||||
.where(*order_conditions, BizInboundOrder.status == DocumentStatus.DRAFT.value)
|
||||
)
|
||||
month_statement = (
|
||||
select(
|
||||
func.count(func.distinct(BizInboundOrder.inbound_id)),
|
||||
func.count(BizInboundItem.inbound_item_id),
|
||||
)
|
||||
.select_from(BizInboundOrder)
|
||||
.outerjoin(BizInboundItem, item_join)
|
||||
.where(
|
||||
*order_conditions,
|
||||
BizInboundOrder.status == DocumentStatus.CONFIRMED.value,
|
||||
BizInboundOrder.confirmed_at >= month_start,
|
||||
)
|
||||
)
|
||||
unstocked_statement = (
|
||||
select(func.count(BizPalletInventory.pallet_id))
|
||||
.select_from(BizPalletInventory)
|
||||
.join(
|
||||
BizPallet,
|
||||
and_(
|
||||
BizPallet.pallet_id == BizPalletInventory.pallet_id,
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
BizPalletInventory.flow_state == PalletFlowState.UNSTOCKED.value,
|
||||
BizPallet.asset_status == '0',
|
||||
)
|
||||
)
|
||||
empty_statement = (
|
||||
select(
|
||||
BizPalletInventory.warehouse_id,
|
||||
BizWarehouse.warehouse_name,
|
||||
func.count(BizPalletInventory.pallet_id),
|
||||
)
|
||||
.select_from(BizPalletInventory)
|
||||
.outerjoin(
|
||||
BizWarehouse,
|
||||
and_(
|
||||
BizWarehouse.warehouse_id == BizPalletInventory.warehouse_id,
|
||||
batch_scope_condition(BizWarehouse.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
*inventory_conditions,
|
||||
BizPalletInventory.flow_state == PalletFlowState.EMPTY_IN_WAREHOUSE.value,
|
||||
BizPalletInventory.warehouse_id.is_not(None),
|
||||
)
|
||||
.group_by(BizPalletInventory.warehouse_id, BizWarehouse.warehouse_name)
|
||||
)
|
||||
|
||||
draft_count, draft_pallets, oldest_draft_at = (await db.execute(draft_statement)).one()
|
||||
month_count, month_pallets = (await db.execute(month_statement)).one()
|
||||
unstocked_count = (await db.execute(unstocked_statement)).scalar_one()
|
||||
empty_rows = [
|
||||
{'warehouse_id': warehouse_id, 'warehouse_name': warehouse_name, 'pallet_count': int(count or 0)}
|
||||
for warehouse_id, warehouse_name, count in (await db.execute(empty_statement)).all()
|
||||
]
|
||||
empty_rows.sort(key=lambda row: (-row['pallet_count'], row['warehouse_id']))
|
||||
return {
|
||||
'draft_count': int(draft_count or 0),
|
||||
'draft_pallet_count': int(draft_pallets or 0),
|
||||
'oldest_draft_at': oldest_draft_at,
|
||||
'month_start': month_start.date(),
|
||||
'month_confirmed_count': int(month_count or 0),
|
||||
'month_confirmed_pallet_count': int(month_pallets or 0),
|
||||
'unstocked_pallet_count': int(unstocked_count or 0),
|
||||
'empty_pallet_count': sum(row['pallet_count'] for row in empty_rows),
|
||||
'empty_by_warehouse': empty_rows,
|
||||
}
|
||||
Reference in New Issue
Block a user