更新登录页及优化相关功能
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user