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,997 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import and_, case, 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, PalletEventType, PalletFlowState, ReturnStatus
|
||||
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
|
||||
from module_business.entity.do.master_do import (
|
||||
BizCustomer,
|
||||
BizCustomerSite,
|
||||
BizProductionLine,
|
||||
BizTunnel,
|
||||
BizUserSite,
|
||||
BizUserWarehouse,
|
||||
BizWarehouse,
|
||||
)
|
||||
from module_business.entity.do.operation_do import (
|
||||
BizFillingRecord,
|
||||
BizInboundOrder,
|
||||
BizPalletEvent,
|
||||
BizPalletInventory,
|
||||
BizReturnItem,
|
||||
BizReturnOrder,
|
||||
BizShipmentItem,
|
||||
BizShipmentOrder,
|
||||
BizUsageRecord,
|
||||
)
|
||||
|
||||
SITE_STATES = (
|
||||
PalletFlowState.AT_SITE_UNUSED.value,
|
||||
PalletFlowState.AT_SITE_PARTIAL.value,
|
||||
PalletFlowState.AT_SITE_EMPTY.value,
|
||||
)
|
||||
FIELD_STOCK_STATES = (
|
||||
PalletFlowState.IN_TRANSIT.value,
|
||||
PalletFlowState.AT_SITE_UNUSED.value,
|
||||
PalletFlowState.AT_SITE_PARTIAL.value,
|
||||
)
|
||||
EXPIRY_WARNING_DAYS = 7
|
||||
RECENT_DAYS = 30
|
||||
USER_PREVIEW_LIMIT = 5
|
||||
RECENT_EVENT_LIMIT = 6
|
||||
PARENT_OPTION_LIMIT = 1000
|
||||
|
||||
|
||||
def _count_when(condition: Any) -> Any:
|
||||
return func.coalesce(func.sum(case((condition, 1), else_=0)), 0)
|
||||
|
||||
|
||||
def _sum_when(condition: Any, column: Any) -> Any:
|
||||
return func.coalesce(func.sum(case((condition, column), else_=0)), 0)
|
||||
|
||||
|
||||
def _active_user() -> Any:
|
||||
return and_(SysUser.status == '0', SysUser.del_flag == '0')
|
||||
|
||||
|
||||
class MasterSummaryDao:
|
||||
"""基础资料列表与详情的只读汇总。
|
||||
|
||||
只汇总调用方已分页得到的主数据 ID;每张关联表都受同一测试批次范围约束,
|
||||
结果只用于展示,不参与任何写入校验。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _ids(resource_ids: list[int]) -> list[int]:
|
||||
return sorted(set(resource_ids))
|
||||
|
||||
@classmethod
|
||||
async def get_strain_summaries(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
strain_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = cls._ids(strain_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
in_warehouse = BizPalletInventory.flow_state == PalletFlowState.FILLED_IN_WAREHOUSE.value
|
||||
in_field = BizPalletInventory.flow_state.in_(FIELD_STOCK_STATES)
|
||||
stock_statement = (
|
||||
select(
|
||||
BizFillingRecord.strain_id,
|
||||
_count_when(in_warehouse),
|
||||
_sum_when(in_warehouse, BizPalletInventory.remaining_packages),
|
||||
_count_when(in_field),
|
||||
_sum_when(in_field, BizPalletInventory.remaining_packages),
|
||||
func.min(case((in_warehouse, BizFillingRecord.expiry_date), else_=None)),
|
||||
)
|
||||
.select_from(BizPalletInventory)
|
||||
.join(
|
||||
BizFillingRecord,
|
||||
and_(
|
||||
BizFillingRecord.filling_id == BizPalletInventory.active_filling_id,
|
||||
BizFillingRecord.pallet_id == BizPalletInventory.pallet_id,
|
||||
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
BizFillingRecord.strain_id.in_(ids),
|
||||
BizPalletInventory.remaining_packages > 0,
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizFillingRecord.strain_id)
|
||||
)
|
||||
recent_from = datetime.now() - timedelta(days=RECENT_DAYS)
|
||||
filling_statement = (
|
||||
select(
|
||||
BizFillingRecord.strain_id,
|
||||
func.max(BizFillingRecord.operated_at),
|
||||
_count_when(BizFillingRecord.operated_at >= recent_from),
|
||||
)
|
||||
.where(
|
||||
BizFillingRecord.strain_id.in_(ids),
|
||||
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizFillingRecord.strain_id)
|
||||
)
|
||||
|
||||
summaries: dict[int, dict[str, Any]] = {strain_id: {} for strain_id in ids}
|
||||
for strain_id, wh_pallets, wh_packages, field_pallets, field_packages, nearest in (
|
||||
await db.execute(stock_statement)
|
||||
).all():
|
||||
summaries[strain_id].update(
|
||||
warehouse_pallet_count=int(wh_pallets),
|
||||
warehouse_package_qty=int(wh_packages),
|
||||
field_pallet_count=int(field_pallets),
|
||||
field_package_qty=int(field_packages),
|
||||
nearest_expiry_date=nearest,
|
||||
)
|
||||
for strain_id, last_filled_at, recent_count in (await db.execute(filling_statement)).all():
|
||||
summaries[strain_id].update(last_filled_at=last_filled_at, filling_count_30d=int(recent_count))
|
||||
return summaries
|
||||
|
||||
@classmethod
|
||||
async def _site_pallet_counts(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
group_column: Any,
|
||||
condition: Any,
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, int]]:
|
||||
state = BizPalletInventory.flow_state
|
||||
statement = (
|
||||
select(
|
||||
group_column,
|
||||
_count_when(state == PalletFlowState.AT_SITE_UNUSED.value),
|
||||
_count_when(state == PalletFlowState.AT_SITE_PARTIAL.value),
|
||||
_count_when(state == PalletFlowState.AT_SITE_EMPTY.value),
|
||||
_count_when(state == PalletFlowState.IN_TRANSIT.value),
|
||||
_sum_when(state.in_(SITE_STATES), BizPalletInventory.remaining_packages),
|
||||
)
|
||||
.select_from(BizPalletInventory)
|
||||
.join(
|
||||
BizCustomerSite,
|
||||
and_(
|
||||
BizCustomerSite.site_id == BizPalletInventory.site_id,
|
||||
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
condition,
|
||||
state.in_((*SITE_STATES, PalletFlowState.IN_TRANSIT.value)),
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
.group_by(group_column)
|
||||
)
|
||||
return {
|
||||
key: {
|
||||
'unused_pallet_count': int(unused),
|
||||
'partial_pallet_count': int(partial),
|
||||
'empty_pallet_count': int(empty),
|
||||
'transit_pallet_count': int(transit),
|
||||
'remaining_package_qty': int(remaining),
|
||||
}
|
||||
for key, unused, partial, empty, transit, remaining in (await db.execute(statement)).all()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def _site_users(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
site_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, list[dict[str, Any]]]:
|
||||
if not site_ids:
|
||||
return {}
|
||||
statement = (
|
||||
select(BizUserSite.site_id, SysUser.user_id, SysUser.user_name, SysUser.nick_name)
|
||||
.select_from(BizUserSite)
|
||||
.join(
|
||||
BizCustomerSite,
|
||||
and_(
|
||||
BizCustomerSite.site_id == BizUserSite.site_id,
|
||||
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.join(SysUser, and_(SysUser.user_id == BizUserSite.user_id, _active_user()))
|
||||
.where(BizUserSite.site_id.in_(site_ids))
|
||||
.order_by(BizUserSite.site_id, SysUser.user_id)
|
||||
)
|
||||
users: dict[int, list[dict[str, Any]]] = {}
|
||||
for site_id, user_id, user_name, nick_name in (await db.execute(statement)).all():
|
||||
users.setdefault(site_id, []).append({'user_id': user_id, 'user_name': user_name, 'nick_name': nick_name})
|
||||
return users
|
||||
|
||||
@classmethod
|
||||
async def _count_by(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
group_column: Any,
|
||||
count_column: Any,
|
||||
*conditions: Any,
|
||||
join: tuple[Any, Any] | None = None,
|
||||
) -> dict[int, int]:
|
||||
statement = select(group_column, func.count(count_column))
|
||||
if join is not None:
|
||||
statement = statement.select_from(count_column.class_).join(*join)
|
||||
statement = statement.where(*conditions).group_by(group_column)
|
||||
return {key: int(count) for key, count in (await db.execute(statement)).all()}
|
||||
|
||||
@classmethod
|
||||
async def get_customer_summaries(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
customer_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = cls._ids(customer_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
site_scope = batch_scope_condition(BizCustomerSite.test_batch_id, scope)
|
||||
site_join = (
|
||||
BizCustomerSite,
|
||||
and_(BizCustomerSite.site_id == BizProductionLine.site_id, site_scope),
|
||||
)
|
||||
site_counts = await cls._count_by(
|
||||
db,
|
||||
BizCustomerSite.customer_id,
|
||||
BizCustomerSite.site_id,
|
||||
BizCustomerSite.customer_id.in_(ids),
|
||||
site_scope,
|
||||
)
|
||||
line_counts = await cls._count_by(
|
||||
db,
|
||||
BizCustomerSite.customer_id,
|
||||
BizProductionLine.line_id,
|
||||
BizCustomerSite.customer_id.in_(ids),
|
||||
batch_scope_condition(BizProductionLine.test_batch_id, scope),
|
||||
join=site_join,
|
||||
)
|
||||
tunnel_counts = await cls._count_by(
|
||||
db,
|
||||
BizCustomerSite.customer_id,
|
||||
BizTunnel.tunnel_id,
|
||||
BizCustomerSite.customer_id.in_(ids),
|
||||
batch_scope_condition(BizTunnel.test_batch_id, scope),
|
||||
join=(BizCustomerSite, and_(BizCustomerSite.site_id == BizTunnel.site_id, site_scope)),
|
||||
)
|
||||
pallet_counts = await cls._site_pallet_counts(
|
||||
db, BizCustomerSite.customer_id, BizCustomerSite.customer_id.in_(ids), scope
|
||||
)
|
||||
user_statement = (
|
||||
select(BizCustomerSite.customer_id, func.count(func.distinct(BizUserSite.user_id)))
|
||||
.select_from(BizUserSite)
|
||||
.join(BizCustomerSite, and_(BizCustomerSite.site_id == BizUserSite.site_id, site_scope))
|
||||
.join(SysUser, and_(SysUser.user_id == BizUserSite.user_id, _active_user()))
|
||||
.where(BizCustomerSite.customer_id.in_(ids))
|
||||
.group_by(BizCustomerSite.customer_id)
|
||||
)
|
||||
user_counts = {key: int(count) for key, count in (await db.execute(user_statement)).all()}
|
||||
|
||||
summaries: dict[int, dict[str, Any]] = {}
|
||||
for customer_id in ids:
|
||||
pallets = pallet_counts.get(customer_id, {})
|
||||
summaries[customer_id] = {
|
||||
'site_count': site_counts.get(customer_id, 0),
|
||||
'line_count': line_counts.get(customer_id, 0),
|
||||
'tunnel_count': tunnel_counts.get(customer_id, 0),
|
||||
'site_pallet_count': pallets.get('unused_pallet_count', 0)
|
||||
+ pallets.get('partial_pallet_count', 0)
|
||||
+ pallets.get('empty_pallet_count', 0),
|
||||
'transit_pallet_count': pallets.get('transit_pallet_count', 0),
|
||||
'user_count': user_counts.get(customer_id, 0),
|
||||
}
|
||||
return summaries
|
||||
|
||||
@classmethod
|
||||
async def get_customer_site_tree(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
customer_id: int,
|
||||
scope: TestBatchScope,
|
||||
) -> list[dict[str, Any]]:
|
||||
site_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizCustomerSite.site_id,
|
||||
BizCustomerSite.site_code,
|
||||
BizCustomerSite.site_name,
|
||||
BizCustomerSite.address,
|
||||
BizCustomerSite.status,
|
||||
)
|
||||
.where(
|
||||
BizCustomerSite.customer_id == customer_id,
|
||||
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
|
||||
)
|
||||
.order_by(BizCustomerSite.site_code, BizCustomerSite.site_id)
|
||||
)
|
||||
).all()
|
||||
sites = {
|
||||
row.site_id: {
|
||||
'site_id': row.site_id,
|
||||
'site_code': row.site_code,
|
||||
'site_name': row.site_name,
|
||||
'address': row.address,
|
||||
'status': row.status,
|
||||
'lines': [],
|
||||
'tunnels': [],
|
||||
'users': [],
|
||||
}
|
||||
for row in site_rows
|
||||
}
|
||||
if not sites:
|
||||
return []
|
||||
site_ids = list(sites)
|
||||
|
||||
line_pallets = (
|
||||
select(
|
||||
BizPalletInventory.current_line_id,
|
||||
func.count(BizPalletInventory.pallet_id).label('pallet_count'),
|
||||
)
|
||||
.where(
|
||||
BizPalletInventory.site_id.in_(site_ids),
|
||||
BizPalletInventory.current_line_id.is_not(None),
|
||||
BizPalletInventory.flow_state.in_(SITE_STATES),
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizPalletInventory.current_line_id)
|
||||
.subquery()
|
||||
)
|
||||
line_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizProductionLine.site_id,
|
||||
BizProductionLine.line_id,
|
||||
BizProductionLine.line_code,
|
||||
BizProductionLine.line_name,
|
||||
BizProductionLine.status,
|
||||
func.coalesce(line_pallets.c.pallet_count, 0),
|
||||
)
|
||||
.outerjoin(line_pallets, line_pallets.c.current_line_id == BizProductionLine.line_id)
|
||||
.where(
|
||||
BizProductionLine.site_id.in_(site_ids),
|
||||
batch_scope_condition(BizProductionLine.test_batch_id, scope),
|
||||
)
|
||||
.order_by(BizProductionLine.line_code, BizProductionLine.line_id)
|
||||
)
|
||||
).all()
|
||||
for site_id, line_id, line_code, line_name, status, pallet_count in line_rows:
|
||||
sites[site_id]['lines'].append(
|
||||
{
|
||||
'line_id': line_id,
|
||||
'line_code': line_code,
|
||||
'line_name': line_name,
|
||||
'status': status,
|
||||
'active_pallet_count': int(pallet_count),
|
||||
}
|
||||
)
|
||||
|
||||
tunnel_pallets = (
|
||||
select(
|
||||
BizPalletInventory.tunnel_id,
|
||||
func.count(BizPalletInventory.pallet_id).label('pallet_count'),
|
||||
)
|
||||
.where(
|
||||
BizPalletInventory.site_id.in_(site_ids),
|
||||
BizPalletInventory.tunnel_id.is_not(None),
|
||||
BizPalletInventory.flow_state.in_(SITE_STATES),
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizPalletInventory.tunnel_id)
|
||||
.subquery()
|
||||
)
|
||||
tunnel_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizTunnel.site_id,
|
||||
BizTunnel.tunnel_id,
|
||||
BizTunnel.tunnel_code,
|
||||
BizTunnel.tunnel_name,
|
||||
BizTunnel.status,
|
||||
BizTunnel.current_trace_no,
|
||||
BizTunnel.current_trace_started_at,
|
||||
func.coalesce(tunnel_pallets.c.pallet_count, 0),
|
||||
)
|
||||
.outerjoin(tunnel_pallets, tunnel_pallets.c.tunnel_id == BizTunnel.tunnel_id)
|
||||
.where(
|
||||
BizTunnel.site_id.in_(site_ids),
|
||||
batch_scope_condition(BizTunnel.test_batch_id, scope),
|
||||
)
|
||||
.order_by(BizTunnel.tunnel_code, BizTunnel.tunnel_id)
|
||||
)
|
||||
).all()
|
||||
for site_id, tunnel_id, tunnel_code, tunnel_name, status, trace_no, trace_started_at, pallet_count in (
|
||||
tunnel_rows
|
||||
):
|
||||
sites[site_id]['tunnels'].append(
|
||||
{
|
||||
'tunnel_id': tunnel_id,
|
||||
'tunnel_code': tunnel_code,
|
||||
'tunnel_name': tunnel_name,
|
||||
'status': status,
|
||||
'current_trace_no': trace_no,
|
||||
'current_trace_started_at': trace_started_at,
|
||||
'active_pallet_count': int(pallet_count),
|
||||
}
|
||||
)
|
||||
|
||||
pallet_counts = await cls._site_pallet_counts(
|
||||
db, BizPalletInventory.site_id, BizPalletInventory.site_id.in_(site_ids), scope
|
||||
)
|
||||
for site_id, counts in pallet_counts.items():
|
||||
sites[site_id].update(counts)
|
||||
for site_id, users in (await cls._site_users(db, site_ids, scope)).items():
|
||||
sites[site_id]['users'] = users
|
||||
return list(sites.values())
|
||||
|
||||
@classmethod
|
||||
async def get_site_summaries(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
site_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = cls._ids(site_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
name_rows = (
|
||||
await db.execute(
|
||||
select(BizCustomerSite.site_id, BizCustomer.customer_code, BizCustomer.customer_name)
|
||||
.select_from(BizCustomerSite)
|
||||
.join(
|
||||
BizCustomer,
|
||||
and_(
|
||||
BizCustomer.customer_id == BizCustomerSite.customer_id,
|
||||
batch_scope_condition(BizCustomer.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(BizCustomerSite.site_id.in_(ids), batch_scope_condition(BizCustomerSite.test_batch_id, scope))
|
||||
)
|
||||
).all()
|
||||
line_counts = await cls._count_by(
|
||||
db,
|
||||
BizProductionLine.site_id,
|
||||
BizProductionLine.line_id,
|
||||
BizProductionLine.site_id.in_(ids),
|
||||
batch_scope_condition(BizProductionLine.test_batch_id, scope),
|
||||
)
|
||||
tunnel_counts = await cls._count_by(
|
||||
db,
|
||||
BizTunnel.site_id,
|
||||
BizTunnel.tunnel_id,
|
||||
BizTunnel.site_id.in_(ids),
|
||||
batch_scope_condition(BizTunnel.test_batch_id, scope),
|
||||
)
|
||||
pallet_counts = await cls._site_pallet_counts(
|
||||
db, BizPalletInventory.site_id, BizPalletInventory.site_id.in_(ids), scope
|
||||
)
|
||||
users = await cls._site_users(db, ids, scope)
|
||||
|
||||
summaries: dict[int, dict[str, Any]] = {
|
||||
site_id: {
|
||||
'line_count': line_counts.get(site_id, 0),
|
||||
'tunnel_count': tunnel_counts.get(site_id, 0),
|
||||
**pallet_counts.get(site_id, {}),
|
||||
'user_count': len(users.get(site_id, [])),
|
||||
'users': users.get(site_id, [])[:USER_PREVIEW_LIMIT],
|
||||
}
|
||||
for site_id in ids
|
||||
}
|
||||
for site_id, customer_code, customer_name in name_rows:
|
||||
summaries[site_id].update(customer_code=customer_code, customer_name=customer_name)
|
||||
return summaries
|
||||
|
||||
@classmethod
|
||||
async def _child_site_names(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
model: Any,
|
||||
id_column: Any,
|
||||
ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
statement = (
|
||||
select(id_column, BizCustomerSite.site_code, BizCustomerSite.site_name, BizCustomer.customer_name)
|
||||
.select_from(model)
|
||||
.join(
|
||||
BizCustomerSite,
|
||||
and_(
|
||||
BizCustomerSite.site_id == model.site_id,
|
||||
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
BizCustomer,
|
||||
and_(
|
||||
BizCustomer.customer_id == BizCustomerSite.customer_id,
|
||||
batch_scope_condition(BizCustomer.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(id_column.in_(ids), batch_scope_condition(model.test_batch_id, scope))
|
||||
)
|
||||
return {
|
||||
resource_id: {'site_code': site_code, 'site_name': site_name, 'customer_name': customer_name}
|
||||
for resource_id, site_code, site_name, customer_name in (await db.execute(statement)).all()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def get_line_summaries(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
line_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = cls._ids(line_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
names = await cls._child_site_names(db, BizProductionLine, BizProductionLine.line_id, ids, scope)
|
||||
pallet_counts = await cls._count_by(
|
||||
db,
|
||||
BizPalletInventory.current_line_id,
|
||||
BizPalletInventory.pallet_id,
|
||||
BizPalletInventory.current_line_id.in_(ids),
|
||||
BizPalletInventory.flow_state.in_(SITE_STATES),
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
recent_from = datetime.now() - timedelta(days=RECENT_DAYS)
|
||||
recent = BizUsageRecord.occurred_at >= recent_from
|
||||
usage_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizUsageRecord.line_id,
|
||||
_count_when(recent),
|
||||
_sum_when(recent, BizUsageRecord.used_packages),
|
||||
func.max(BizUsageRecord.occurred_at),
|
||||
)
|
||||
.where(BizUsageRecord.line_id.in_(ids), batch_scope_condition(BizUsageRecord.test_batch_id, scope))
|
||||
.group_by(BizUsageRecord.line_id)
|
||||
)
|
||||
).all()
|
||||
summaries = {
|
||||
line_id: {**names.get(line_id, {}), 'active_pallet_count': pallet_counts.get(line_id, 0)} for line_id in ids
|
||||
}
|
||||
for line_id, usage_count, used_packages, last_used_at in usage_rows:
|
||||
summaries[line_id].update(
|
||||
usage_count_30d=int(usage_count),
|
||||
used_package_qty_30d=int(used_packages),
|
||||
last_used_at=last_used_at,
|
||||
)
|
||||
return summaries
|
||||
|
||||
@classmethod
|
||||
async def get_tunnel_summaries(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
tunnel_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = cls._ids(tunnel_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
names = await cls._child_site_names(db, BizTunnel, BizTunnel.tunnel_id, ids, scope)
|
||||
pallet_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizPalletInventory.tunnel_id,
|
||||
func.count(BizPalletInventory.pallet_id),
|
||||
func.coalesce(func.sum(BizPalletInventory.remaining_packages), 0),
|
||||
)
|
||||
.where(
|
||||
BizPalletInventory.tunnel_id.in_(ids),
|
||||
BizPalletInventory.flow_state.in_(SITE_STATES),
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizPalletInventory.tunnel_id)
|
||||
)
|
||||
).all()
|
||||
recent_from = datetime.now() - timedelta(days=RECENT_DAYS)
|
||||
usage_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizUsageRecord.tunnel_id,
|
||||
_count_when(BizUsageRecord.occurred_at >= recent_from),
|
||||
func.max(BizUsageRecord.occurred_at),
|
||||
)
|
||||
.where(
|
||||
BizUsageRecord.tunnel_id.in_(ids),
|
||||
batch_scope_condition(BizUsageRecord.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizUsageRecord.tunnel_id)
|
||||
)
|
||||
).all()
|
||||
summaries: dict[int, dict[str, Any]] = {tunnel_id: dict(names.get(tunnel_id, {})) for tunnel_id in ids}
|
||||
for tunnel_id, pallet_count, package_qty in pallet_rows:
|
||||
summaries[tunnel_id].update(active_pallet_count=int(pallet_count), active_package_qty=int(package_qty))
|
||||
for tunnel_id, usage_count, last_used_at in usage_rows:
|
||||
summaries[tunnel_id].update(usage_count_30d=int(usage_count), last_used_at=last_used_at)
|
||||
return summaries
|
||||
|
||||
@classmethod
|
||||
async def get_warehouse_summaries(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
warehouse_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = cls._ids(warehouse_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
today_start = datetime.combine(date.today(), datetime.min.time())
|
||||
expiry_limit = date.today() + timedelta(days=EXPIRY_WARNING_DAYS)
|
||||
filled = BizPalletInventory.flow_state == PalletFlowState.FILLED_IN_WAREHOUSE.value
|
||||
stock_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizPalletInventory.warehouse_id,
|
||||
_count_when(filled),
|
||||
_sum_when(filled, BizPalletInventory.remaining_packages),
|
||||
_count_when(BizPalletInventory.flow_state == PalletFlowState.EMPTY_IN_WAREHOUSE.value),
|
||||
_count_when(and_(filled, BizFillingRecord.expiry_date <= expiry_limit)),
|
||||
)
|
||||
.select_from(BizPalletInventory)
|
||||
.outerjoin(
|
||||
BizFillingRecord,
|
||||
and_(
|
||||
BizFillingRecord.filling_id == BizPalletInventory.active_filling_id,
|
||||
BizFillingRecord.pallet_id == BizPalletInventory.pallet_id,
|
||||
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
BizPalletInventory.warehouse_id.in_(ids),
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
.group_by(BizPalletInventory.warehouse_id)
|
||||
)
|
||||
).all()
|
||||
filling_counts = await cls._count_by(
|
||||
db,
|
||||
BizFillingRecord.warehouse_id,
|
||||
BizFillingRecord.filling_id,
|
||||
BizFillingRecord.warehouse_id.in_(ids),
|
||||
BizFillingRecord.operated_at >= today_start,
|
||||
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
||||
)
|
||||
shipment_counts = await cls._count_by(
|
||||
db,
|
||||
BizShipmentOrder.source_warehouse_id,
|
||||
BizShipmentItem.shipment_item_id,
|
||||
BizShipmentOrder.source_warehouse_id.in_(ids),
|
||||
BizShipmentOrder.status == DocumentStatus.CONFIRMED.value,
|
||||
BizShipmentOrder.confirmed_at >= today_start,
|
||||
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
|
||||
join=(
|
||||
BizShipmentOrder,
|
||||
and_(
|
||||
BizShipmentOrder.shipment_id == BizShipmentItem.shipment_id,
|
||||
batch_scope_condition(BizShipmentOrder.test_batch_id, scope),
|
||||
),
|
||||
),
|
||||
)
|
||||
inbound_drafts = await cls._count_by(
|
||||
db,
|
||||
BizInboundOrder.warehouse_id,
|
||||
BizInboundOrder.inbound_id,
|
||||
BizInboundOrder.warehouse_id.in_(ids),
|
||||
BizInboundOrder.status == DocumentStatus.DRAFT.value,
|
||||
batch_scope_condition(BizInboundOrder.test_batch_id, scope),
|
||||
)
|
||||
shipment_drafts = await cls._count_by(
|
||||
db,
|
||||
BizShipmentOrder.source_warehouse_id,
|
||||
BizShipmentOrder.shipment_id,
|
||||
BizShipmentOrder.source_warehouse_id.in_(ids),
|
||||
BizShipmentOrder.status == DocumentStatus.DRAFT.value,
|
||||
batch_scope_condition(BizShipmentOrder.test_batch_id, scope),
|
||||
)
|
||||
incoming_returns = await cls._count_by(
|
||||
db,
|
||||
BizReturnOrder.target_warehouse_id,
|
||||
BizReturnOrder.return_id,
|
||||
BizReturnOrder.target_warehouse_id.in_(ids),
|
||||
BizReturnOrder.status == ReturnStatus.REQUESTED.value,
|
||||
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
|
||||
)
|
||||
user_rows = (
|
||||
await db.execute(
|
||||
select(BizUserWarehouse.warehouse_id, SysUser.user_id, SysUser.user_name, SysUser.nick_name)
|
||||
.select_from(BizUserWarehouse)
|
||||
.join(
|
||||
BizWarehouse,
|
||||
and_(
|
||||
BizWarehouse.warehouse_id == BizUserWarehouse.warehouse_id,
|
||||
batch_scope_condition(BizWarehouse.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.join(SysUser, and_(SysUser.user_id == BizUserWarehouse.user_id, _active_user()))
|
||||
.where(BizUserWarehouse.warehouse_id.in_(ids))
|
||||
.order_by(BizUserWarehouse.warehouse_id, SysUser.user_id)
|
||||
)
|
||||
).all()
|
||||
users: dict[int, list[dict[str, Any]]] = {}
|
||||
for warehouse_id, user_id, user_name, nick_name in user_rows:
|
||||
users.setdefault(warehouse_id, []).append(
|
||||
{'user_id': user_id, 'user_name': user_name, 'nick_name': nick_name}
|
||||
)
|
||||
|
||||
summaries: dict[int, dict[str, Any]] = {
|
||||
warehouse_id: {
|
||||
'today_filling_count': filling_counts.get(warehouse_id, 0),
|
||||
'today_shipment_pallet_count': shipment_counts.get(warehouse_id, 0),
|
||||
'draft_count': inbound_drafts.get(warehouse_id, 0) + shipment_drafts.get(warehouse_id, 0),
|
||||
'incoming_return_count': incoming_returns.get(warehouse_id, 0),
|
||||
'user_count': len(users.get(warehouse_id, [])),
|
||||
'users': users.get(warehouse_id, [])[:USER_PREVIEW_LIMIT],
|
||||
}
|
||||
for warehouse_id in ids
|
||||
}
|
||||
for warehouse_id, filled_count, package_qty, empty_count, expiring_count in stock_rows:
|
||||
summaries[warehouse_id].update(
|
||||
filled_pallet_count=int(filled_count),
|
||||
filled_package_qty=int(package_qty),
|
||||
empty_pallet_count=int(empty_count),
|
||||
expiring_pallet_count=int(expiring_count),
|
||||
)
|
||||
return summaries
|
||||
|
||||
@classmethod
|
||||
async def get_pallet_summaries(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
pallet_ids: list[int],
|
||||
scope: TestBatchScope,
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
ids = cls._ids(pallet_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
state_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizPalletInventory.pallet_id,
|
||||
BizPalletInventory.flow_state,
|
||||
BizPalletInventory.remaining_packages,
|
||||
BizWarehouse.warehouse_name,
|
||||
BizCustomer.customer_name,
|
||||
BizCustomerSite.site_name,
|
||||
BizProductionLine.line_name,
|
||||
BizTunnel.tunnel_name,
|
||||
BizFillingRecord.strain_name_snapshot,
|
||||
BizFillingRecord.batch_no,
|
||||
BizFillingRecord.expiry_date,
|
||||
)
|
||||
.select_from(BizPalletInventory)
|
||||
.outerjoin(
|
||||
BizWarehouse,
|
||||
and_(
|
||||
BizWarehouse.warehouse_id == BizPalletInventory.warehouse_id,
|
||||
batch_scope_condition(BizWarehouse.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
BizCustomerSite,
|
||||
and_(
|
||||
BizCustomerSite.site_id == BizPalletInventory.site_id,
|
||||
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
BizCustomer,
|
||||
and_(
|
||||
BizCustomer.customer_id == BizCustomerSite.customer_id,
|
||||
batch_scope_condition(BizCustomer.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
BizProductionLine,
|
||||
and_(
|
||||
BizProductionLine.line_id == BizPalletInventory.current_line_id,
|
||||
batch_scope_condition(BizProductionLine.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
BizTunnel,
|
||||
and_(
|
||||
BizTunnel.tunnel_id == BizPalletInventory.tunnel_id,
|
||||
batch_scope_condition(BizTunnel.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
BizFillingRecord,
|
||||
and_(
|
||||
BizFillingRecord.filling_id == BizPalletInventory.active_filling_id,
|
||||
BizFillingRecord.pallet_id == BizPalletInventory.pallet_id,
|
||||
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
BizPalletInventory.pallet_id.in_(ids),
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
)
|
||||
)
|
||||
).all()
|
||||
shipped = BizPalletEvent.event_type == PalletEventType.SHIPMENT_CONFIRMED.value
|
||||
event_rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizPalletEvent.pallet_id,
|
||||
_count_when(shipped),
|
||||
func.max(case((shipped, BizPalletEvent.occurred_at), else_=None)),
|
||||
func.max(BizPalletEvent.occurred_at),
|
||||
)
|
||||
.where(BizPalletEvent.pallet_id.in_(ids), batch_scope_condition(BizPalletEvent.test_batch_id, scope))
|
||||
.group_by(BizPalletEvent.pallet_id)
|
||||
)
|
||||
).all()
|
||||
return_rows = (
|
||||
await db.execute(
|
||||
select(BizReturnItem.pallet_id, BizWarehouse.warehouse_name)
|
||||
.select_from(BizReturnItem)
|
||||
.join(
|
||||
BizReturnOrder,
|
||||
and_(
|
||||
BizReturnOrder.return_id == BizReturnItem.return_id,
|
||||
BizReturnOrder.status == ReturnStatus.REQUESTED.value,
|
||||
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.join(
|
||||
BizWarehouse,
|
||||
and_(
|
||||
BizWarehouse.warehouse_id == BizReturnOrder.target_warehouse_id,
|
||||
batch_scope_condition(BizWarehouse.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(BizReturnItem.pallet_id.in_(ids), batch_scope_condition(BizReturnItem.test_batch_id, scope))
|
||||
)
|
||||
).all()
|
||||
|
||||
summaries: dict[int, dict[str, Any]] = {pallet_id: {} for pallet_id in ids}
|
||||
for row in state_rows:
|
||||
summaries[row[0]].update(
|
||||
flow_state=row[1],
|
||||
remaining_packages=row[2] or 0,
|
||||
warehouse_name=row[3],
|
||||
customer_name=row[4],
|
||||
site_name=row[5],
|
||||
line_name=row[6],
|
||||
tunnel_name=row[7],
|
||||
strain_name=row[8],
|
||||
batch_no=row[9],
|
||||
expiry_date=row[10],
|
||||
)
|
||||
for pallet_id, cycle_count, last_shipped_at, last_event_at in event_rows:
|
||||
summaries[pallet_id].update(
|
||||
cycle_count=int(cycle_count),
|
||||
last_shipped_at=last_shipped_at,
|
||||
last_event_at=last_event_at,
|
||||
)
|
||||
for pallet_id, warehouse_name in return_rows:
|
||||
summaries[pallet_id]['return_warehouse_name'] = warehouse_name
|
||||
return summaries
|
||||
|
||||
@classmethod
|
||||
async def get_pallet_recent_events(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
pallet_id: int,
|
||||
scope: TestBatchScope,
|
||||
) -> list[dict[str, Any]]:
|
||||
warehouse = aliased(BizWarehouse, name='event_warehouse')
|
||||
site = aliased(BizCustomerSite, name='event_site')
|
||||
customer = aliased(BizCustomer, name='event_customer')
|
||||
line = aliased(BizProductionLine, name='event_line')
|
||||
tunnel = aliased(BizTunnel, name='event_tunnel')
|
||||
operator = aliased(SysUser, name='event_operator')
|
||||
statement = (
|
||||
select(
|
||||
BizPalletEvent.event_id,
|
||||
BizPalletEvent.event_type,
|
||||
BizPalletEvent.occurred_at,
|
||||
BizPalletEvent.after_state,
|
||||
BizPalletEvent.before_remaining_packages,
|
||||
BizPalletEvent.after_remaining_packages,
|
||||
BizPalletEvent.trace_no,
|
||||
warehouse.warehouse_name,
|
||||
customer.customer_name,
|
||||
site.site_name,
|
||||
line.line_name,
|
||||
tunnel.tunnel_name,
|
||||
operator.nick_name,
|
||||
)
|
||||
.select_from(BizPalletEvent)
|
||||
.outerjoin(
|
||||
warehouse,
|
||||
and_(
|
||||
warehouse.warehouse_id
|
||||
== func.coalesce(BizPalletEvent.after_warehouse_id, BizPalletEvent.before_warehouse_id),
|
||||
batch_scope_condition(warehouse.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
site,
|
||||
and_(
|
||||
site.site_id == func.coalesce(BizPalletEvent.after_site_id, BizPalletEvent.before_site_id),
|
||||
batch_scope_condition(site.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
customer,
|
||||
and_(customer.customer_id == site.customer_id, batch_scope_condition(customer.test_batch_id, scope)),
|
||||
)
|
||||
.outerjoin(
|
||||
line,
|
||||
and_(line.line_id == BizPalletEvent.after_line_id, batch_scope_condition(line.test_batch_id, scope)),
|
||||
)
|
||||
.outerjoin(
|
||||
tunnel,
|
||||
and_(
|
||||
tunnel.tunnel_id == BizPalletEvent.after_tunnel_id,
|
||||
batch_scope_condition(tunnel.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.outerjoin(operator, operator.user_id == BizPalletEvent.operator_id)
|
||||
.where(BizPalletEvent.pallet_id == pallet_id, batch_scope_condition(BizPalletEvent.test_batch_id, scope))
|
||||
.order_by(BizPalletEvent.event_seq.desc())
|
||||
.limit(RECENT_EVENT_LIMIT)
|
||||
)
|
||||
keys = (
|
||||
'event_id',
|
||||
'event_type',
|
||||
'occurred_at',
|
||||
'after_state',
|
||||
'before_remaining_packages',
|
||||
'after_remaining_packages',
|
||||
'trace_no',
|
||||
'warehouse_name',
|
||||
'customer_name',
|
||||
'site_name',
|
||||
'line_name',
|
||||
'tunnel_name',
|
||||
'operator_name',
|
||||
)
|
||||
return [dict(zip(keys, row, strict=True)) for row in (await db.execute(statement)).all()]
|
||||
|
||||
@classmethod
|
||||
async def list_customer_options(cls, db: AsyncSession, scope: TestBatchScope) -> list[dict[str, Any]]:
|
||||
"""新增或修改现场时可选的客户:当前数据范围内状态正常的客户。"""
|
||||
rows = (
|
||||
await db.execute(
|
||||
select(BizCustomer.customer_id, BizCustomer.customer_code, BizCustomer.customer_name)
|
||||
.where(BizCustomer.status == '0', batch_scope_condition(BizCustomer.test_batch_id, scope))
|
||||
.order_by(BizCustomer.customer_code, BizCustomer.customer_id)
|
||||
.limit(PARENT_OPTION_LIMIT)
|
||||
)
|
||||
).all()
|
||||
return [{'id': row[0], 'code': row[1], 'name': row[2]} for row in rows]
|
||||
|
||||
@classmethod
|
||||
async def list_site_options(cls, db: AsyncSession, scope: TestBatchScope) -> list[dict[str, Any]]:
|
||||
"""新增或修改产线、隧道时可选的现场:状态正常且所属客户也正常的现场。"""
|
||||
rows = (
|
||||
await db.execute(
|
||||
select(
|
||||
BizCustomerSite.site_id,
|
||||
BizCustomerSite.site_code,
|
||||
BizCustomerSite.site_name,
|
||||
BizCustomer.customer_name,
|
||||
)
|
||||
.select_from(BizCustomerSite)
|
||||
.join(
|
||||
BizCustomer,
|
||||
and_(
|
||||
BizCustomer.customer_id == BizCustomerSite.customer_id,
|
||||
BizCustomer.status == '0',
|
||||
batch_scope_condition(BizCustomer.test_batch_id, scope),
|
||||
),
|
||||
)
|
||||
.where(BizCustomerSite.status == '0', batch_scope_condition(BizCustomerSite.test_batch_id, scope))
|
||||
.order_by(BizCustomer.customer_code, BizCustomerSite.site_code, BizCustomerSite.site_id)
|
||||
.limit(PARENT_OPTION_LIMIT)
|
||||
)
|
||||
).all()
|
||||
return [{'id': row[0], 'code': row[1], 'name': row[2], 'parent_name': row[3]} for row in rows]
|
||||
Reference in New Issue
Block a user