更新登录页及优化相关功能

This commit is contained in:
焦龙言
2026-10-08 14:13:05 +08:00
parent 9a14b692e3
commit 56e646bcc1
139 changed files with 77587 additions and 811 deletions
@@ -1,6 +1,8 @@
from sqlalchemy import select
from sqlalchemy import or_, select
from sqlalchemy.exc import MultipleResultsFound
from sqlalchemy.ext.asyncio import AsyncSession
from exceptions.exception import ServiceException
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 BizPalletEvent, BizPalletInventory
@@ -57,13 +59,18 @@ class InventoryDao:
qr_token: str,
scope: TestBatchScope,
) -> BizPallet | None:
"""兼容印刷托盘号;调用方仍须复核仓库/现场权限,写入使用规范二维码。"""
identifier = qr_token.strip()
result = await db.execute(
select(BizPallet).where(
BizPallet.qr_token == qr_token,
or_(BizPallet.qr_token == identifier, BizPallet.pallet_code == identifier),
batch_scope_condition(BizPallet.test_batch_id, scope),
)
)
return result.scalar_one_or_none()
try:
return result.scalar_one_or_none()
except MultipleResultsFound:
raise ServiceException(message='托盘号与二维码标识存在歧义,请联系管理员核对后重试') from None
@classmethod
async def get_pallets_by_ids(
@@ -2,7 +2,7 @@ from datetime import datetime
from typing import Any
from pydantic import BaseModel
from sqlalchemy import ColumnElement, Select, select, update
from sqlalchemy import ColumnElement, Select, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
@@ -15,6 +15,7 @@ from module_business.entity.do.master_do import (
BizPallet,
BizProductionLine,
BizStrain,
BizTunnel,
BizUserSite,
BizUserWarehouse,
BizWarehouse,
@@ -31,6 +32,8 @@ from module_business.entity.vo.master_vo import (
SitePageQueryModel,
StrainModel,
StrainPageQueryModel,
TunnelModel,
TunnelPageQueryModel,
WarehouseModel,
WarehousePageQueryModel,
)
@@ -38,7 +41,7 @@ from utils.page_util import PageUtil
class MasterDao:
"""菌种、客户、现场、产线和仓库主数据数据库操作层。"""
"""菌种、客户、现场、产线、隧道和仓库主数据数据库操作层。"""
@staticmethod
async def _paginate(
@@ -339,6 +342,67 @@ class MasterDao:
async def update_line(cls, db: AsyncSession, line: LineModel, scope: TestBatchScope) -> None:
await cls._update(db, BizProductionLine, BizProductionLine.line_id, 'line_id', line, scope)
@classmethod
async def list_tunnel(
cls,
db: AsyncSession,
query_object: TunnelPageQueryModel,
scope: TestBatchScope,
is_page: bool = True,
) -> PageModel | list[dict[str, Any]]:
conditions = [batch_scope_condition(BizTunnel.test_batch_id, scope)]
if query_object.site_id:
conditions.append(BizTunnel.site_id == query_object.site_id)
if query_object.tunnel_code:
conditions.append(BizTunnel.tunnel_code.like(f'%{query_object.tunnel_code}%'))
if query_object.tunnel_name:
conditions.append(BizTunnel.tunnel_name.like(f'%{query_object.tunnel_name}%'))
if query_object.status:
conditions.append(BizTunnel.status == query_object.status)
statement = select(BizTunnel).where(*conditions).order_by(BizTunnel.tunnel_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_tunnel(
cls,
db: AsyncSession,
tunnel_id: int,
scope: TestBatchScope,
*,
for_update: bool = False,
) -> BizTunnel | None:
return await cls._get(
db,
BizTunnel,
BizTunnel.tunnel_id,
tunnel_id,
batch_scope_condition(BizTunnel.test_batch_id, scope),
for_update=for_update,
)
@classmethod
async def find_tunnel_by_code(
cls,
db: AsyncSession,
tunnel_code: str,
site_id: int,
) -> BizTunnel | None:
return await cls._find(
db,
BizTunnel,
BizTunnel.tunnel_code,
tunnel_code,
BizTunnel.site_id == site_id,
)
@classmethod
async def add_tunnel(cls, db: AsyncSession, tunnel: TunnelModel, test_batch_id: int | None) -> BizTunnel:
return await cls._add(db, BizTunnel, 'tunnel_id', tunnel, test_batch_id)
@classmethod
async def update_tunnel(cls, db: AsyncSession, tunnel: TunnelModel, scope: TestBatchScope) -> None:
await cls._update(db, BizTunnel, BizTunnel.tunnel_id, 'tunnel_id', tunnel, scope)
@classmethod
async def list_warehouse(
cls,
@@ -475,6 +539,25 @@ class MasterDao:
result = await db.execute(statement)
return result.scalar_one_or_none()
@classmethod
async def count_tunnel_trace_members(
cls,
db: AsyncSession,
tunnel_id: int,
trace_no: str,
scope: TestBatchScope,
) -> int:
result = await db.execute(
select(func.count())
.select_from(BizPalletInventory)
.where(
BizPalletInventory.tunnel_id == tunnel_id,
BizPalletInventory.trace_no == trace_no,
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
)
return int(result.scalar_one() or 0)
@classmethod
async def add_pallet_inventory(
cls,
@@ -810,6 +810,10 @@ class OperationDao:
conditions.append(BizUsageRecord.usage_no.like(f'%{query_object.usage_no}%'))
if query_object.site_id:
conditions.append(BizUsageRecord.site_id == query_object.site_id)
if query_object.tunnel_id:
conditions.append(BizUsageRecord.tunnel_id == query_object.tunnel_id)
if query_object.trace_no:
conditions.append(BizUsageRecord.trace_no.like(f'%{query_object.trace_no}%'))
if query_object.line_id:
conditions.append(BizUsageRecord.line_id == query_object.line_id)
if query_object.pallet_code:
@@ -1,7 +1,7 @@
from datetime import datetime, time, timedelta
from typing import Any
from sqlalchemy import ColumnElement, Select, and_, false, or_, select
from sqlalchemy import ColumnElement, Select, and_, case, false, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased
from sqlalchemy.orm.attributes import InstrumentedAttribute
@@ -15,6 +15,7 @@ from module_business.entity.do.master_do import (
BizCustomerSite,
BizPallet,
BizProductionLine,
BizTunnel,
BizWarehouse,
)
from module_business.entity.do.operation_do import (
@@ -40,6 +41,7 @@ from module_business.entity.vo.report_vo import (
ReturnReportQueryModel,
ShipmentReportModel,
ShipmentReportQueryModel,
TunnelReportQueryModel,
)
from utils.page_util import PageUtil
@@ -84,6 +86,31 @@ class ReportDao:
"""保留空的可选 join,同时拒绝已存在的跨批次关联记录。"""
return or_(id_column.is_(None), batch_scope_condition(scope_column, scope))
@staticmethod
def _tunnel_conditions(
query: TunnelReportQueryModel,
trace_column: ColumnElement,
*tunnels: tuple[ColumnElement, Any],
) -> list[ColumnElement[bool]]:
"""历史事件可匹配变更前/后隧道;同一组编号、名称须命中同一个隧道。"""
conditions = []
if query.trace_no:
conditions.append(trace_column == query.trace_no)
matches = []
for id_column, tunnel in tunnels:
filters = []
if query.tunnel_id:
filters.append(id_column == query.tunnel_id)
if query.tunnel_code:
filters.append(tunnel.tunnel_code.like(f'%{query.tunnel_code}%'))
if query.tunnel_name:
filters.append(tunnel.tunnel_name.like(f'%{query.tunnel_name}%'))
if filters:
matches.append(and_(*filters))
if matches:
conditions.append(or_(*matches))
return conditions
@classmethod
def _or_scope_conditions(
cls,
@@ -158,6 +185,7 @@ class ReportDao:
current_warehouse = aliased(BizWarehouse, name='current_warehouse')
current_site = aliased(BizCustomerSite, name='current_site')
current_line = aliased(BizProductionLine, name='current_line')
current_tunnel = aliased(BizTunnel, name='current_tunnel')
active_filling = aliased(BizFillingRecord, name='active_filling')
conditions = [
@@ -167,7 +195,11 @@ class ReportDao:
cls._joined_scope_condition(current_warehouse.warehouse_id, current_warehouse.test_batch_id, scope),
cls._joined_scope_condition(current_site.site_id, current_site.test_batch_id, scope),
cls._joined_scope_condition(current_line.line_id, current_line.test_batch_id, scope),
cls._joined_scope_condition(current_tunnel.tunnel_id, current_tunnel.test_batch_id, scope),
]
conditions.extend(cls._tunnel_conditions(
query_object, BizPalletInventory.trace_no, (BizPalletInventory.tunnel_id, current_tunnel),
))
conditions.extend(
cls._or_scope_conditions(
(BizPalletInventory.warehouse_id, warehouse_ids),
@@ -214,6 +246,12 @@ class ReportDao:
active_filling.initial_packages.label('initial_packages'),
BizPalletInventory.remaining_packages.label('remaining_packages'),
BizPalletInventory.updated_at.label('updated_at'),
BizPalletInventory.tunnel_id.label('tunnel_id'),
current_tunnel.tunnel_code.label('tunnel_code'),
current_tunnel.tunnel_name.label('tunnel_name'),
BizPalletInventory.trace_no.label('trace_no'),
active_filling.production_date.label('production_date'),
active_filling.expiry_date.label('expiry_date'),
)
.select_from(BizPalletInventory)
.join(BizPallet, BizPallet.pallet_id == BizPalletInventory.pallet_id)
@@ -221,6 +259,7 @@ class ReportDao:
.outerjoin(current_warehouse, current_warehouse.warehouse_id == BizPalletInventory.warehouse_id)
.outerjoin(current_site, current_site.site_id == BizPalletInventory.site_id)
.outerjoin(current_line, current_line.line_id == BizPalletInventory.current_line_id)
.outerjoin(current_tunnel, current_tunnel.tunnel_id == BizPalletInventory.tunnel_id)
.where(*conditions)
.order_by(BizPalletInventory.pallet_id)
)
@@ -374,14 +413,21 @@ class ReportDao:
site_ids: list[int] | None = None,
) -> PageModel[ConsumptionReportModel]:
operator_user = aliased(SysUser, name='operator_user')
usage_tunnel = aliased(BizTunnel, name='usage_tunnel')
# 新记录优先使用业务快照;历史空值只回查该次使用引用的装填记录。
batch_no = func.coalesce(BizUsageRecord.batch_no_snapshot, BizFillingRecord.batch_no)
conditions = [
batch_scope_condition(BizUsageRecord.test_batch_id, scope),
batch_scope_condition(BizPallet.test_batch_id, scope),
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
batch_scope_condition(BizProductionLine.test_batch_id, scope),
cls._joined_scope_condition(BizProductionLine.line_id, BizProductionLine.test_batch_id, scope),
cls._joined_scope_condition(usage_tunnel.tunnel_id, usage_tunnel.test_batch_id, scope),
cls._joined_scope_condition(BizCustomer.customer_id, BizCustomer.test_batch_id, scope),
]
conditions.extend(cls._tunnel_conditions(
query_object, BizUsageRecord.trace_no, (BizUsageRecord.tunnel_id, usage_tunnel),
))
site_scope = cls._scope_condition(BizUsageRecord.site_id, site_ids)
if site_scope is not None:
conditions.append(site_scope)
@@ -396,7 +442,7 @@ class ReportDao:
if query_object.strain_id:
conditions.append(BizFillingRecord.strain_id == query_object.strain_id)
if query_object.batch_no:
conditions.append(BizFillingRecord.batch_no.like(f'%{query_object.batch_no}%'))
conditions.append(batch_no.like(f'%{query_object.batch_no}%'))
conditions.extend(
cls._time_conditions(
BizUsageRecord.occurred_at,
@@ -421,17 +467,25 @@ class ReportDao:
BizFillingRecord.strain_id.label('strain_id'),
BizFillingRecord.strain_code_snapshot.label('strain_code'),
BizFillingRecord.strain_name_snapshot.label('strain_name'),
BizFillingRecord.batch_no.label('batch_no'),
batch_no.label('batch_no'),
BizUsageRecord.used_packages.label('used_packages'),
BizUsageRecord.remaining_packages_after.label('remaining_packages_after'),
BizUsageRecord.operator_id.label('operator_id'),
operator_user.user_name.label('operator_name'),
BizUsageRecord.tunnel_id.label('tunnel_id'),
usage_tunnel.tunnel_code.label('tunnel_code'),
usage_tunnel.tunnel_name.label('tunnel_name'),
BizUsageRecord.trace_no.label('trace_no'),
BizUsageRecord.batch_no_snapshot.label('batch_no_snapshot'),
BizFillingRecord.production_date.label('production_date'),
BizFillingRecord.expiry_date.label('expiry_date'),
)
.select_from(BizUsageRecord)
.join(BizPallet, BizPallet.pallet_id == BizUsageRecord.pallet_id)
.join(BizFillingRecord, BizFillingRecord.filling_id == BizUsageRecord.active_filling_id)
.join(BizCustomerSite, BizCustomerSite.site_id == BizUsageRecord.site_id)
.join(BizProductionLine, BizProductionLine.line_id == BizUsageRecord.line_id)
.outerjoin(BizProductionLine, BizProductionLine.line_id == BizUsageRecord.line_id)
.outerjoin(usage_tunnel, usage_tunnel.tunnel_id == BizUsageRecord.tunnel_id)
.outerjoin(BizCustomer, BizCustomer.customer_id == BizCustomerSite.customer_id)
.outerjoin(operator_user, operator_user.user_id == BizUsageRecord.operator_id)
.where(*conditions)
@@ -450,13 +504,23 @@ class ReportDao:
) -> PageModel[ReturnReportModel]:
source_site = aliased(BizCustomerSite, name='source_site')
target_warehouse = aliased(BizWarehouse, name='target_warehouse')
return_tunnel = aliased(BizTunnel, name='return_tunnel')
return_filling = aliased(BizFillingRecord, name='return_filling')
batch_no = func.coalesce(BizReturnItem.batch_no_snapshot, return_filling.batch_no)
conditions = [
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
batch_scope_condition(BizReturnItem.test_batch_id, scope),
batch_scope_condition(BizPallet.test_batch_id, scope),
cls._joined_scope_condition(source_site.site_id, source_site.test_batch_id, scope),
cls._joined_scope_condition(target_warehouse.warehouse_id, target_warehouse.test_batch_id, scope),
cls._joined_scope_condition(return_tunnel.tunnel_id, return_tunnel.test_batch_id, scope),
cls._joined_scope_condition(return_filling.filling_id, return_filling.test_batch_id, scope),
]
conditions.extend(cls._tunnel_conditions(
query_object, BizReturnItem.trace_no_snapshot, (BizReturnItem.tunnel_id_snapshot, return_tunnel),
))
if query_object.batch_no:
conditions.append(batch_no.like(f'%{query_object.batch_no}%'))
conditions.extend(
cls._or_scope_conditions(
(BizReturnOrder.source_site_id, site_ids),
@@ -496,12 +560,22 @@ class ReportDao:
BizPallet.pallet_code.label('pallet_code'),
BizReturnItem.active_filling_id_snapshot.label('active_filling_id_snapshot'),
BizReturnItem.remaining_packages_snapshot.label('remaining_packages_snapshot'),
BizReturnItem.tunnel_id_snapshot.label('tunnel_id'),
return_tunnel.tunnel_code.label('tunnel_code'),
return_tunnel.tunnel_name.label('tunnel_name'),
BizReturnItem.trace_no_snapshot.label('trace_no'),
BizReturnItem.batch_no_snapshot.label('batch_no_snapshot'),
batch_no.label('batch_no'),
return_filling.production_date.label('production_date'),
return_filling.expiry_date.label('expiry_date'),
)
.select_from(BizReturnOrder)
.join(BizReturnItem, BizReturnItem.return_id == BizReturnOrder.return_id)
.join(BizPallet, BizPallet.pallet_id == BizReturnItem.pallet_id)
.outerjoin(source_site, source_site.site_id == BizReturnOrder.source_site_id)
.outerjoin(target_warehouse, target_warehouse.warehouse_id == BizReturnOrder.target_warehouse_id)
.outerjoin(return_tunnel, return_tunnel.tunnel_id == BizReturnItem.tunnel_id_snapshot)
.outerjoin(return_filling, return_filling.filling_id == BizReturnItem.active_filling_id_snapshot)
.where(*conditions)
.order_by(BizReturnOrder.return_id, BizReturnItem.return_item_id)
)
@@ -522,6 +596,41 @@ class ReportDao:
after_site = aliased(BizCustomerSite, name='after_site')
before_line = aliased(BizProductionLine, name='before_line')
after_line = aliased(BizProductionLine, name='after_line')
before_tunnel = aliased(BizTunnel, name='before_tunnel')
after_tunnel = aliased(BizTunnel, name='after_tunnel')
trace_return_item = aliased(BizReturnItem, name='trace_return_item')
trace_usage = aliased(BizUsageRecord, name='trace_usage')
trace_shipment_item = aliased(BizShipmentItem, name='trace_shipment_item')
trace_filling = aliased(BizFillingRecord, name='trace_filling')
prior_filling_event = aliased(BizPalletEvent, name='prior_filling_event')
# 入隧事件来源是隧道而非装填记录,按托盘事件序号回查当时已发生的最后一次装填。
# 其他事件只使用来源单据的装填引用;引用缺失时保留空日期,不借用后续重装填。
prior_filling_id = (
select(prior_filling_event.source_id)
.where(
prior_filling_event.pallet_id == BizPalletEvent.pallet_id,
prior_filling_event.event_seq < BizPalletEvent.event_seq,
prior_filling_event.event_type == PalletEventType.FILLING_CONFIRMED.value,
prior_filling_event.source_type == 'FILLING',
batch_scope_condition(prior_filling_event.test_batch_id, scope),
)
.order_by(prior_filling_event.event_seq.desc())
.limit(1)
.correlate(BizPalletEvent)
.scalar_subquery()
)
trace_filling_id = case(
(BizPalletEvent.source_type == 'FILLING', BizPalletEvent.source_id),
(BizPalletEvent.source_type == 'USAGE', trace_usage.active_filling_id),
(BizPalletEvent.source_type == 'RETURN', trace_return_item.active_filling_id_snapshot),
(BizPalletEvent.source_type == 'SHIPMENT', trace_shipment_item.active_filling_id),
(BizPalletEvent.event_type == PalletEventType.TUNNEL_ENTERED.value, prior_filling_id),
else_=None,
)
# 部分退回事件未保存隧道;只从同单同托盘的冻结退回项补读,不读取当前库存。
before_tunnel_id = func.coalesce(BizPalletEvent.before_tunnel_id, trace_return_item.tunnel_id_snapshot)
trace_no = func.coalesce(BizPalletEvent.trace_no, trace_return_item.trace_no_snapshot)
batch_no = func.coalesce(BizPalletEvent.batch_no_snapshot, trace_return_item.batch_no_snapshot)
operator_user = aliased(SysUser, name='operator_user')
conditions = [
batch_scope_condition(BizPalletEvent.test_batch_id, scope),
@@ -532,7 +641,20 @@ class ReportDao:
cls._joined_scope_condition(after_site.site_id, after_site.test_batch_id, scope),
cls._joined_scope_condition(before_line.line_id, before_line.test_batch_id, scope),
cls._joined_scope_condition(after_line.line_id, after_line.test_batch_id, scope),
cls._joined_scope_condition(before_tunnel.tunnel_id, before_tunnel.test_batch_id, scope),
cls._joined_scope_condition(after_tunnel.tunnel_id, after_tunnel.test_batch_id, scope),
cls._joined_scope_condition(trace_return_item.return_item_id, trace_return_item.test_batch_id, scope),
cls._joined_scope_condition(trace_usage.usage_id, trace_usage.test_batch_id, scope),
cls._joined_scope_condition(trace_shipment_item.shipment_item_id, trace_shipment_item.test_batch_id, scope),
cls._joined_scope_condition(trace_filling.filling_id, trace_filling.test_batch_id, scope),
]
conditions.extend(cls._tunnel_conditions(
query_object, trace_no,
(before_tunnel_id, before_tunnel),
(BizPalletEvent.after_tunnel_id, after_tunnel),
))
if query_object.batch_no:
conditions.append(batch_no.like(f'%{query_object.batch_no}%'))
conditions.extend(cls._pallet_trace_scope_conditions(warehouse_ids, site_ids))
if query_object.pallet_code:
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
@@ -574,6 +696,19 @@ class ReportDao:
BizPalletEvent.operator_id.label('operator_id'),
operator_user.user_name.label('operator_name'),
BizPalletEvent.occurred_at.label('occurred_at'),
func.coalesce(BizPalletEvent.after_tunnel_id, before_tunnel_id).label('tunnel_id'),
func.coalesce(after_tunnel.tunnel_code, before_tunnel.tunnel_code).label('tunnel_code'),
func.coalesce(after_tunnel.tunnel_name, before_tunnel.tunnel_name).label('tunnel_name'),
trace_no.label('trace_no'),
batch_no.label('batch_no_snapshot'),
before_tunnel_id.label('before_tunnel_id'),
before_tunnel.tunnel_code.label('before_tunnel_code'),
before_tunnel.tunnel_name.label('before_tunnel_name'),
BizPalletEvent.after_tunnel_id.label('after_tunnel_id'),
after_tunnel.tunnel_code.label('after_tunnel_code'),
after_tunnel.tunnel_name.label('after_tunnel_name'),
trace_filling.production_date.label('production_date'),
trace_filling.expiry_date.label('expiry_date'),
)
.select_from(BizPalletEvent)
.join(BizPallet, BizPallet.pallet_id == BizPalletEvent.pallet_id)
@@ -583,6 +718,27 @@ class ReportDao:
.outerjoin(after_site, after_site.site_id == BizPalletEvent.after_site_id)
.outerjoin(before_line, before_line.line_id == BizPalletEvent.before_line_id)
.outerjoin(after_line, after_line.line_id == BizPalletEvent.after_line_id)
.outerjoin(trace_return_item, and_(
BizPalletEvent.source_type == 'RETURN',
trace_return_item.return_id == BizPalletEvent.source_id,
trace_return_item.pallet_id == BizPalletEvent.pallet_id,
))
.outerjoin(trace_usage, and_(
BizPalletEvent.source_type == 'USAGE',
trace_usage.usage_id == BizPalletEvent.source_id,
trace_usage.pallet_id == BizPalletEvent.pallet_id,
))
.outerjoin(trace_shipment_item, and_(
BizPalletEvent.source_type == 'SHIPMENT',
trace_shipment_item.shipment_id == BizPalletEvent.source_id,
trace_shipment_item.pallet_id == BizPalletEvent.pallet_id,
))
.outerjoin(trace_filling, and_(
trace_filling.filling_id == trace_filling_id,
trace_filling.pallet_id == BizPalletEvent.pallet_id,
))
.outerjoin(before_tunnel, before_tunnel.tunnel_id == before_tunnel_id)
.outerjoin(after_tunnel, after_tunnel.tunnel_id == BizPalletEvent.after_tunnel_id)
.outerjoin(operator_user, operator_user.user_id == BizPalletEvent.operator_id)
.where(*conditions)
.order_by(BizPalletEvent.occurred_at, BizPalletEvent.event_id)
@@ -0,0 +1,62 @@
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
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.operation_do import (
BizFillingRecord,
BizPalletInventory,
BizShipmentItem,
BizShipmentOrder,
)
class ShipmentLabelDao:
"""只读标签资料;外连接保留失效明细,由服务拒绝不完整的整单打印。"""
@staticmethod
async def get_label_sources(
db: AsyncSession,
shipment: BizShipmentOrder,
scope: TestBatchScope,
) -> list[tuple[BizShipmentItem, BizPallet | None, BizFillingRecord | None, int | None]]:
draft = shipment.status == DocumentStatus.DRAFT.value
quantity = BizPalletInventory.remaining_packages if draft else BizShipmentItem.package_qty
statement = (
select(BizShipmentItem, BizPallet, BizFillingRecord, quantity)
.select_from(BizShipmentItem)
.outerjoin(BizPallet, and_(
BizShipmentItem.pallet_id == BizPallet.pallet_id,
batch_scope_condition(BizPallet.test_batch_id, scope),
))
)
if draft:
statement = statement.outerjoin(BizPalletInventory, and_(
BizShipmentItem.pallet_id == BizPalletInventory.pallet_id,
BizPalletInventory.warehouse_id == shipment.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),
))
# 已确认单只关联确认时的装填记录,即使周期已关闭、托盘已重新装填也可补打。
filling_id = BizPalletInventory.active_filling_id if draft else BizShipmentItem.active_filling_id
filling_conditions = [
filling_id == BizFillingRecord.filling_id,
BizShipmentItem.pallet_id == BizFillingRecord.pallet_id,
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
]
if draft:
filling_conditions.append(BizFillingRecord.active_slot == 'ACTIVE')
statement = (
statement.outerjoin(BizFillingRecord, and_(*filling_conditions))
.where(
BizShipmentItem.shipment_id == shipment.shipment_id,
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.shipment_item_id)
)
return list((await db.execute(statement)).all())
@@ -0,0 +1,36 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import PageModel
from module_business.dao.operation_dao import OperationDao
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.operation_do import BizShipmentOrder
from module_business.entity.vo.shipment_receipt_vo import ShipmentReceiptQueryModel
from utils.page_util import PageUtil
class ShipmentReceiptDao:
@staticmethod
async def list_orders(
db: AsyncSession, query: ShipmentReceiptQueryModel, scope: TestBatchScope, site_ids: list[int] | None
) -> PageModel:
# 接收方只需要目标现场范围,不扩大原发货操作的双范围权限。
conditions = OperationDao._shipment_scope_conditions(None, site_ids, scope)
conditions.append(BizShipmentOrder.status == 'CONFIRMED')
if query.shipment_no:
conditions.append(BizShipmentOrder.shipment_no.like(f'%{query.shipment_no}%'))
if query.target_site_id:
conditions.append(BizShipmentOrder.target_site_id == query.target_site_id)
if query.source_warehouse_id:
conditions.append(BizShipmentOrder.source_warehouse_id == query.source_warehouse_id)
if query.receipt_state:
conditions.append(BizShipmentOrder.receipt_state == query.receipt_state)
conditions.extend(
OperationDao._time_conditions(BizShipmentOrder.confirmed_at, query.begin_time, query.end_time)
)
statement = (
select(BizShipmentOrder)
.where(*conditions)
.order_by(BizShipmentOrder.confirmed_at, BizShipmentOrder.shipment_id)
)
return await PageUtil.paginate(db, statement, query.page_num, query.page_size, is_page=True)
@@ -0,0 +1,85 @@
"""按真实收货时间推荐可用物料,不推断历史到货时间。"""
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())