Files
ShiErFeng/shierfeng-fastapi-backend/module_business/dao/operation_dao.py
T

871 lines
29 KiB
Python

from datetime import datetime, time, timedelta
from typing import Any
from sqlalchemy import ColumnElement, Select, and_, delete, false, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
from module_business.domain.enums import DocumentStatus, PalletFlowState
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import (
BizFillingRecord,
BizInboundItem,
BizInboundOrder,
BizPalletInventory,
BizReturnItem,
BizReturnOrder,
BizShipmentItem,
BizShipmentOrder,
BizUsageRecord,
)
from module_business.entity.vo.operation_vo import (
FillingPageQueryModel,
InboundPageQueryModel,
ReturnPageQueryModel,
ShipmentPageQueryModel,
UsagePageQueryModel,
)
from utils.page_util import PageUtil
class OperationDao:
"""托盘入库、装填、发货、使用和退回单据数据库操作层。"""
@staticmethod
def _time_conditions(
column: InstrumentedAttribute,
begin_time: str | None,
end_time: str | None,
) -> list[ColumnElement[bool]]:
conditions = []
if begin_time:
begin_at = datetime.combine(
datetime.strptime(begin_time, '%Y-%m-%d').date(),
time.min,
)
conditions.append(column >= begin_at)
if end_time:
end_at = datetime.combine(
datetime.strptime(end_time, '%Y-%m-%d').date() + timedelta(days=1),
time.min,
)
conditions.append(column < end_at)
return conditions
@staticmethod
def _inbound_scope_conditions(
warehouse_ids: list[int] | None,
) -> list[ColumnElement[bool]]:
if warehouse_ids is None:
return []
scoped_warehouse_ids = sorted(set(warehouse_ids))
return [BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()]
@staticmethod
def _filling_scope_conditions(
warehouse_ids: list[int] | None,
) -> list[ColumnElement[bool]]:
if warehouse_ids is None:
return []
scoped_warehouse_ids = sorted(set(warehouse_ids))
return [BizFillingRecord.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()]
@staticmethod
def _usage_scope_conditions(
site_ids: list[int] | None,
) -> list[ColumnElement[bool]]:
if site_ids is None:
return []
scoped_site_ids = sorted(set(site_ids))
return [BizUsageRecord.site_id.in_(scoped_site_ids) if scoped_site_ids else false()]
@staticmethod
def _shipment_scope_conditions(
warehouse_ids: list[int] | None,
site_ids: list[int] | None,
) -> list[ColumnElement[bool]]:
conditions = []
if warehouse_ids is not None:
scoped_warehouse_ids = sorted(set(warehouse_ids))
conditions.append(
BizShipmentOrder.source_warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()
)
if site_ids is not None:
scoped_site_ids = sorted(set(site_ids))
conditions.append(BizShipmentOrder.target_site_id.in_(scoped_site_ids) if scoped_site_ids else false())
return conditions
@staticmethod
def _return_scope_conditions(
site_ids: list[int] | None,
warehouse_ids: list[int] | None,
) -> list[ColumnElement[bool]]:
if site_ids is None and warehouse_ids is None:
return []
visibility_conditions: list[ColumnElement[bool]] = []
if site_ids is not None:
scoped_site_ids = sorted(set(site_ids))
visibility_conditions.append(
BizReturnOrder.source_site_id.in_(scoped_site_ids) if scoped_site_ids else false()
)
if warehouse_ids is not None:
scoped_warehouse_ids = sorted(set(warehouse_ids))
visibility_conditions.append(
BizReturnOrder.target_warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()
)
return [or_(*visibility_conditions)]
@staticmethod
async def _paginate(
db: AsyncSession,
statement: Select,
query_object: Any,
is_page: bool,
) -> PageModel | list[dict[str, Any]]:
return await PageUtil.paginate(
db,
statement,
query_object.page_num,
query_object.page_size,
is_page,
)
@staticmethod
async def _get_for_update(
db: AsyncSession,
model: type[Any],
id_column: InstrumentedAttribute,
record_id: int,
*extra_conditions: ColumnElement[bool],
) -> Any | None:
result = await db.execute(
select(model)
.where(id_column == record_id, *extra_conditions)
.with_for_update()
.execution_options(populate_existing=True)
)
return result.scalar_one_or_none()
@staticmethod
async def _get_by_key(
db: AsyncSession,
model: type[Any],
key_column: InstrumentedAttribute,
idempotency_key: str,
*extra_conditions: ColumnElement[bool],
) -> Any | None:
result = await db.execute(select(model).where(key_column == idempotency_key, *extra_conditions))
return result.scalar_one_or_none()
@staticmethod
async def _get_by_id(
db: AsyncSession,
model: type[Any],
id_column: InstrumentedAttribute,
record_id: int,
*extra_conditions: ColumnElement[bool],
) -> Any | None:
result = await db.execute(select(model).where(id_column == record_id, *extra_conditions))
return result.scalar_one_or_none()
@classmethod
async def _get_order_detail(
cls,
db: AsyncSession,
order_model: type[Any],
order_id_column: InstrumentedAttribute,
order_id: int,
item_model: type[Any],
item_order_id_column: InstrumentedAttribute,
item_id_column: InstrumentedAttribute,
*order_conditions: ColumnElement[bool],
) -> Any | None:
order = await cls._get_by_id(
db,
order_model,
order_id_column,
order_id,
*order_conditions,
)
if order is None:
return None
item_result = await db.execute(
select(item_model).where(item_order_id_column == order_id).order_by(item_id_column)
)
order.items = list(item_result.scalars().all())
return order
@staticmethod
async def _add(db: AsyncSession, record: Any) -> Any:
db.add(record)
await db.flush()
return record
@staticmethod
async def _update(
db: AsyncSession,
model: type[Any],
id_column: InstrumentedAttribute,
record: Any,
) -> Any:
primary_key = id_column.key
payload = {
column.key: vars(record)[column.key]
for column in model.__table__.columns
if column.key not in {primary_key, 'create_by', 'create_time'} and column.key in vars(record)
}
if payload:
await db.execute(update(model).where(id_column == getattr(record, primary_key)).values(**payload))
await db.flush()
return record
@staticmethod
async def _delete(
db: AsyncSession,
model: type[Any],
id_column: InstrumentedAttribute,
record_id: int,
) -> None:
await db.execute(delete(model).where(id_column == record_id))
await db.flush()
@staticmethod
async def _replace_items(
db: AsyncSession,
item_model: type[Any],
order_id_column: InstrumentedAttribute,
order_id: int,
items: list[Any],
) -> list[Any]:
await db.execute(delete(item_model).where(order_id_column == order_id))
for item in items:
setattr(item, order_id_column.key, order_id)
if items:
db.add_all(items)
await db.flush()
return items
@staticmethod
async def _add_items(db: AsyncSession, items: list[Any]) -> list[Any]:
if items:
db.add_all(items)
await db.flush()
return items
@classmethod
async def get_inbound_for_update(
cls,
db: AsyncSession,
inbound_id: int,
warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None:
inbound = await cls._get_for_update(
db,
BizInboundOrder,
BizInboundOrder.inbound_id,
inbound_id,
*cls._inbound_scope_conditions(warehouse_ids),
)
if inbound is None:
return None
# 父单锁定后用 locking read 获取最新明细,避免 MySQL RR 继续读取确认前的旧快照。
item_result = await db.execute(
select(BizInboundItem)
.where(BizInboundItem.inbound_id == inbound_id)
.order_by(BizInboundItem.pallet_id, BizInboundItem.inbound_item_id)
.with_for_update()
.execution_options(populate_existing=True)
)
inbound.items = sorted(
item_result.scalars().all(),
key=lambda item: (item.pallet_id, item.inbound_item_id),
)
return inbound
@classmethod
async def get_shipment_for_update(
cls,
db: AsyncSession,
shipment_id: int,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
shipment = await cls._get_for_update(
db,
BizShipmentOrder,
BizShipmentOrder.shipment_id,
shipment_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids),
)
if shipment is None:
return None
# 父单锁定后读取当前明细,避免 MySQL RR 沿用确认前的快照。
item_result = await db.execute(
select(BizShipmentItem)
.where(BizShipmentItem.shipment_id == shipment_id)
.order_by(BizShipmentItem.pallet_id, BizShipmentItem.shipment_item_id)
.with_for_update()
.execution_options(populate_existing=True)
)
shipment.items = sorted(
item_result.scalars().all(),
key=lambda item: (item.pallet_id, item.shipment_item_id),
)
return shipment
@classmethod
async def get_return_for_update(
cls,
db: AsyncSession,
return_id: int,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None:
return_order = await cls._get_for_update(
db,
BizReturnOrder,
BizReturnOrder.return_id,
return_id,
*cls._return_scope_conditions(site_ids, warehouse_ids),
)
if return_order is None:
return None
# 父单锁定后使用 locking read 获取当前明细,避免 MySQL RR 沿用旧快照。
item_result = await db.execute(
select(BizReturnItem)
.where(BizReturnItem.return_id == return_id)
.order_by(BizReturnItem.pallet_id, BizReturnItem.return_item_id)
.with_for_update()
.execution_options(populate_existing=True)
)
return_order.items = sorted(
item_result.scalars().all(),
key=lambda item: (item.pallet_id, item.return_item_id),
)
return return_order
@classmethod
async def get_active_filling_for_update(
cls,
db: AsyncSession,
filling_id: int,
) -> BizFillingRecord | None:
"""按已锁库存的 active_filling_id 锁定仍处于活动状态的装填周期。"""
return await cls._get_for_update(
db,
BizFillingRecord,
BizFillingRecord.filling_id,
filling_id,
BizFillingRecord.active_slot == 'ACTIVE',
)
@classmethod
async def get_inbound_by_confirm_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
) -> BizInboundOrder | None:
return await cls._get_by_key(
db,
BizInboundOrder,
BizInboundOrder.confirm_idempotency_key,
idempotency_key,
)
@classmethod
async def get_shipment_by_confirm_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
return await cls._get_by_key(
db,
BizShipmentOrder,
BizShipmentOrder.confirm_idempotency_key,
idempotency_key,
*cls._shipment_scope_conditions(warehouse_ids, site_ids),
)
@classmethod
async def get_filling_by_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
warehouse_ids: list[int] | None = None,
) -> BizFillingRecord | None:
return await cls._get_by_key(
db,
BizFillingRecord,
BizFillingRecord.idempotency_key,
idempotency_key,
*cls._filling_scope_conditions(warehouse_ids),
)
@classmethod
async def get_usage_by_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
site_ids: list[int] | None = None,
) -> BizUsageRecord | None:
return await cls._get_by_key(
db,
BizUsageRecord,
BizUsageRecord.idempotency_key,
idempotency_key,
*cls._usage_scope_conditions(site_ids),
)
@classmethod
async def get_return_by_request_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
BizReturnOrder,
BizReturnOrder.request_idempotency_key,
idempotency_key,
*cls._return_scope_conditions(site_ids, warehouse_ids),
)
@classmethod
async def get_return_by_confirm_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
BizReturnOrder,
BizReturnOrder.confirm_idempotency_key,
idempotency_key,
*cls._return_scope_conditions(site_ids, warehouse_ids),
)
@classmethod
async def get_inbound_list(
cls,
db: AsyncSession,
query_object: InboundPageQueryModel,
is_page: bool = True,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._inbound_scope_conditions(warehouse_ids)
if query_object.inbound_no:
conditions.append(BizInboundOrder.inbound_no.like(f'%{query_object.inbound_no}%'))
if query_object.warehouse_id:
conditions.append(BizInboundOrder.warehouse_id == query_object.warehouse_id)
if query_object.status:
conditions.append(BizInboundOrder.status == query_object.status.value)
conditions.extend(
cls._time_conditions(
BizInboundOrder.create_time,
query_object.begin_time,
query_object.end_time,
)
)
statement = select(BizInboundOrder).where(*conditions).order_by(BizInboundOrder.inbound_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_inbound_by_id(
cls,
db: AsyncSession,
inbound_id: int,
warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None:
return await cls._get_order_detail(
db,
BizInboundOrder,
BizInboundOrder.inbound_id,
inbound_id,
BizInboundItem,
BizInboundItem.inbound_id,
BizInboundItem.inbound_item_id,
*cls._inbound_scope_conditions(warehouse_ids),
)
@classmethod
async def add_inbound(
cls,
db: AsyncSession,
inbound: BizInboundOrder,
) -> BizInboundOrder:
return await cls._add(db, inbound)
@classmethod
async def update_inbound(
cls,
db: AsyncSession,
inbound: BizInboundOrder,
) -> BizInboundOrder:
return await cls._update(
db,
BizInboundOrder,
BizInboundOrder.inbound_id,
inbound,
)
@classmethod
async def delete_inbound(cls, db: AsyncSession, inbound_id: int) -> None:
await db.execute(delete(BizInboundItem).where(BizInboundItem.inbound_id == inbound_id))
await cls._delete(db, BizInboundOrder, BizInboundOrder.inbound_id, inbound_id)
@classmethod
async def replace_inbound_items(
cls,
db: AsyncSession,
inbound_id: int,
items: list[BizInboundItem],
) -> list[BizInboundItem]:
return await cls._replace_items(
db,
BizInboundItem,
BizInboundItem.inbound_id,
inbound_id,
items,
)
@classmethod
async def get_filling_list(
cls,
db: AsyncSession,
query_object: FillingPageQueryModel,
is_page: bool = True,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._filling_scope_conditions(warehouse_ids)
if query_object.filling_no:
conditions.append(BizFillingRecord.filling_no.like(f'%{query_object.filling_no}%'))
if query_object.warehouse_id:
conditions.append(BizFillingRecord.warehouse_id == query_object.warehouse_id)
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}%'))
if query_object.pallet_code:
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
conditions.extend(
cls._time_conditions(
BizFillingRecord.operated_at,
query_object.begin_time,
query_object.end_time,
)
)
statement = select(BizFillingRecord)
if query_object.pallet_code:
statement = statement.join(
BizPallet,
BizPallet.pallet_id == BizFillingRecord.pallet_id,
)
statement = statement.where(*conditions).order_by(BizFillingRecord.filling_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_filling_by_id(
cls,
db: AsyncSession,
filling_id: int,
warehouse_ids: list[int] | None = None,
) -> BizFillingRecord | None:
return await cls._get_by_id(
db,
BizFillingRecord,
BizFillingRecord.filling_id,
filling_id,
*cls._filling_scope_conditions(warehouse_ids),
)
@classmethod
async def add_filling_record(
cls,
db: AsyncSession,
filling: BizFillingRecord,
) -> BizFillingRecord:
return await cls._add(db, filling)
@classmethod
async def update_filling_record(
cls,
db: AsyncSession,
filling: BizFillingRecord,
) -> BizFillingRecord:
return await cls._update(
db,
BizFillingRecord,
BizFillingRecord.filling_id,
filling,
)
@classmethod
async def get_shipment_list(
cls,
db: AsyncSession,
query_object: ShipmentPageQueryModel,
is_page: bool = True,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._shipment_scope_conditions(warehouse_ids, site_ids)
if query_object.shipment_no:
conditions.append(BizShipmentOrder.shipment_no.like(f'%{query_object.shipment_no}%'))
if query_object.source_warehouse_id:
conditions.append(BizShipmentOrder.source_warehouse_id == query_object.source_warehouse_id)
if query_object.target_site_id:
conditions.append(BizShipmentOrder.target_site_id == query_object.target_site_id)
if query_object.status:
conditions.append(BizShipmentOrder.status == query_object.status.value)
conditions.extend(
cls._time_conditions(
BizShipmentOrder.create_time,
query_object.begin_time,
query_object.end_time,
)
)
statement = select(BizShipmentOrder).where(*conditions).order_by(BizShipmentOrder.shipment_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_shipment_by_id(
cls,
db: AsyncSession,
shipment_id: int,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> BizShipmentOrder | None:
shipment = await cls._get_by_id(
db,
BizShipmentOrder,
BizShipmentOrder.shipment_id,
shipment_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids),
)
if shipment is None:
return None
if shipment.status == DocumentStatus.DRAFT.value:
item_result = await db.execute(
select(BizShipmentItem, BizPalletInventory.remaining_packages)
.outerjoin(
BizPalletInventory,
and_(
BizPalletInventory.pallet_id == BizShipmentItem.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,
),
)
.where(BizShipmentItem.shipment_id == shipment_id)
.order_by(BizShipmentItem.shipment_item_id)
)
items = []
for item, remaining_packages in item_result.all():
item.remaining_packages = remaining_packages
items.append(item)
else:
item_result = await db.execute(
select(BizShipmentItem)
.where(BizShipmentItem.shipment_id == shipment_id)
.order_by(BizShipmentItem.shipment_item_id)
)
items = list(item_result.scalars().all())
for item in items:
item.remaining_packages = None
shipment.items = items
return shipment
@classmethod
async def add_shipment(
cls,
db: AsyncSession,
shipment: BizShipmentOrder,
) -> BizShipmentOrder:
return await cls._add(db, shipment)
@classmethod
async def update_shipment(
cls,
db: AsyncSession,
shipment: BizShipmentOrder,
) -> BizShipmentOrder:
return await cls._update(
db,
BizShipmentOrder,
BizShipmentOrder.shipment_id,
shipment,
)
@classmethod
async def delete_shipment(cls, db: AsyncSession, shipment_id: int) -> None:
await db.execute(delete(BizShipmentItem).where(BizShipmentItem.shipment_id == shipment_id))
await cls._delete(
db,
BizShipmentOrder,
BizShipmentOrder.shipment_id,
shipment_id,
)
@classmethod
async def replace_shipment_items(
cls,
db: AsyncSession,
shipment_id: int,
items: list[BizShipmentItem],
) -> list[BizShipmentItem]:
return await cls._replace_items(
db,
BizShipmentItem,
BizShipmentItem.shipment_id,
shipment_id,
items,
)
@classmethod
async def get_usage_list(
cls,
db: AsyncSession,
query_object: UsagePageQueryModel,
is_page: bool = True,
site_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._usage_scope_conditions(site_ids)
if query_object.usage_no:
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.line_id:
conditions.append(BizUsageRecord.line_id == query_object.line_id)
if query_object.pallet_code:
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
conditions.extend(
cls._time_conditions(
BizUsageRecord.occurred_at,
query_object.begin_time,
query_object.end_time,
)
)
statement = select(BizUsageRecord)
if query_object.pallet_code:
statement = statement.join(
BizPallet,
BizPallet.pallet_id == BizUsageRecord.pallet_id,
)
statement = statement.where(*conditions).order_by(BizUsageRecord.usage_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_usage_by_id(
cls,
db: AsyncSession,
usage_id: int,
site_ids: list[int] | None = None,
) -> BizUsageRecord | None:
return await cls._get_by_id(
db,
BizUsageRecord,
BizUsageRecord.usage_id,
usage_id,
*cls._usage_scope_conditions(site_ids),
)
@classmethod
async def add_usage_record(
cls,
db: AsyncSession,
usage: BizUsageRecord,
) -> BizUsageRecord:
return await cls._add(db, usage)
@classmethod
async def get_return_list(
cls,
db: AsyncSession,
query_object: ReturnPageQueryModel,
is_page: bool = True,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]:
conditions = cls._return_scope_conditions(site_ids, warehouse_ids)
if query_object.return_no:
conditions.append(BizReturnOrder.return_no.like(f'%{query_object.return_no}%'))
if query_object.source_site_id:
conditions.append(BizReturnOrder.source_site_id == query_object.source_site_id)
if query_object.target_warehouse_id:
conditions.append(BizReturnOrder.target_warehouse_id == query_object.target_warehouse_id)
if query_object.status:
conditions.append(BizReturnOrder.status == query_object.status.value)
conditions.extend(
cls._time_conditions(
BizReturnOrder.requested_at,
query_object.begin_time,
query_object.end_time,
)
)
statement = select(BizReturnOrder).where(*conditions).order_by(BizReturnOrder.return_id)
return await cls._paginate(db, statement, query_object, is_page)
@classmethod
async def get_return_by_id(
cls,
db: AsyncSession,
return_id: int,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None:
return await cls._get_order_detail(
db,
BizReturnOrder,
BizReturnOrder.return_id,
return_id,
BizReturnItem,
BizReturnItem.return_id,
BizReturnItem.return_item_id,
*cls._return_scope_conditions(site_ids, warehouse_ids),
)
@classmethod
async def add_return_order(
cls,
db: AsyncSession,
return_order: BizReturnOrder,
) -> BizReturnOrder:
return await cls._add(db, return_order)
@classmethod
async def update_return_order(
cls,
db: AsyncSession,
return_order: BizReturnOrder,
) -> BizReturnOrder:
return await cls._update(
db,
BizReturnOrder,
BizReturnOrder.return_id,
return_order,
)
@classmethod
async def add_return_items(
cls,
db: AsyncSession,
items: list[BizReturnItem],
) -> list[BizReturnItem]:
return await cls._add_items(db, items)