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

792 lines
25 KiB
Python

from datetime import datetime, time, timedelta
from typing import Any
from sqlalchemy import ColumnElement, Select, delete, false, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import (
BizFillingRecord,
BizInboundItem,
BizInboundOrder,
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
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],
for_update: bool = False,
) -> Any | None:
statement = select(model).where(key_column == idempotency_key, *extra_conditions)
if for_update:
statement = statement.with_for_update().execution_options(populate_existing=True)
result = await db.execute(statement)
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,
) -> BizReturnOrder | None:
return await cls._get_for_update(
db,
BizReturnOrder,
BizReturnOrder.return_id,
return_id,
)
@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,
*,
for_update: bool = False,
) -> BizUsageRecord | None:
return await cls._get_by_key(
db,
BizUsageRecord,
BizUsageRecord.idempotency_key,
idempotency_key,
*cls._usage_scope_conditions(site_ids),
for_update=for_update,
)
@classmethod
async def get_return_by_request_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
BizReturnOrder,
BizReturnOrder.request_idempotency_key,
idempotency_key,
)
@classmethod
async def get_return_by_confirm_idempotency_key(
cls,
db: AsyncSession,
idempotency_key: str,
) -> BizReturnOrder | None:
return await cls._get_by_key(
db,
BizReturnOrder,
BizReturnOrder.confirm_idempotency_key,
idempotency_key,
)
@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:
return await cls._get_order_detail(
db,
BizShipmentOrder,
BizShipmentOrder.shipment_id,
shipment_id,
BizShipmentItem,
BizShipmentItem.shipment_id,
BizShipmentItem.shipment_item_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids),
)
@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,
) -> PageModel | list[dict[str, Any]]:
conditions = []
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,
) -> BizReturnOrder | None:
return await cls._get_order_detail(
db,
BizReturnOrder,
BizReturnOrder.return_id,
return_id,
BizReturnItem,
BizReturnItem.return_id,
BizReturnItem.return_item_id,
)
@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)