86 lines
3.5 KiB
Python
86 lines
3.5 KiB
Python
"""按真实收货时间推荐可用物料,不推断历史到货时间。"""
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy import and_, func, or_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
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.operation_do import BizFillingRecord, BizPalletInventory
|
|
|
|
|
|
class UsageFifoDao:
|
|
@staticmethod
|
|
async def recommendation(
|
|
db: AsyncSession,
|
|
*,
|
|
site_id: int,
|
|
strain_id: int,
|
|
tunnel_id: int | None,
|
|
trace_no: str | None,
|
|
scope: TestBatchScope,
|
|
for_update: bool = False,
|
|
) -> tuple[dict[str, Any] | None, int]:
|
|
inventory, filling, pallet = BizPalletInventory, BizFillingRecord, BizPallet
|
|
unbound = and_(inventory.tunnel_id.is_(None), inventory.trace_no.is_(None))
|
|
conditions = [
|
|
inventory.site_id == site_id,
|
|
inventory.warehouse_id.is_(None),
|
|
inventory.flow_state.in_(['AT_SITE_UNUSED', 'AT_SITE_PARTIAL']),
|
|
inventory.remaining_packages > 0,
|
|
inventory.remaining_packages <= pallet.capacity_packages,
|
|
filling.strain_id == strain_id,
|
|
filling.active_slot == 'ACTIVE',
|
|
filling.closed_at.is_(None),
|
|
pallet.asset_status == '0',
|
|
batch_scope_condition(inventory.test_batch_id, scope),
|
|
batch_scope_condition(filling.test_batch_id, scope),
|
|
batch_scope_condition(pallet.test_batch_id, scope),
|
|
(
|
|
unbound
|
|
if tunnel_id is None
|
|
else or_(unbound, and_(inventory.tunnel_id == tunnel_id, inventory.trace_no == trace_no))
|
|
),
|
|
]
|
|
base = (
|
|
select(
|
|
pallet.pallet_id,
|
|
pallet.pallet_code,
|
|
filling.batch_no,
|
|
inventory.remaining_packages,
|
|
inventory.site_arrived_at,
|
|
)
|
|
.select_from(inventory)
|
|
.join(pallet, pallet.pallet_id == inventory.pallet_id)
|
|
.join(
|
|
filling,
|
|
and_(filling.filling_id == inventory.active_filling_id, filling.pallet_id == inventory.pallet_id),
|
|
)
|
|
.where(*conditions)
|
|
)
|
|
statement = (
|
|
base.where(inventory.site_arrived_at.is_not(None))
|
|
.order_by(
|
|
inventory.site_arrived_at,
|
|
inventory.pallet_id,
|
|
)
|
|
.limit(1)
|
|
)
|
|
if for_update:
|
|
# 写入决策必须用当前读,不能复用权限预检期间建立的 MVCC 旧快照。
|
|
# 不使用 skip_locked,避免把正在操作的早到物料悄悄跳过。
|
|
# 此时已持有所选库存及现场/隧道锁,不能等待其他候选库存造成反向锁依赖。
|
|
# NOWAIT 不会跳过早到托盘:冲突令整个命令回滚,再按原幂等键有限重试。
|
|
statement = statement.with_for_update(nowait=True)
|
|
known = await db.execute(statement)
|
|
first = known.mappings().first()
|
|
if for_update:
|
|
return (dict(first) if first is not None else None, 0)
|
|
unknown = await db.execute(
|
|
select(func.count()).select_from(
|
|
base.where(inventory.site_arrived_at.is_(None)).subquery(),
|
|
)
|
|
)
|
|
return (dict(first) if first is not None else None, unknown.scalar_one())
|