功能:实现空托盘首次入库
This commit is contained in:
@@ -0,0 +1,197 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Path, Query, Request, Response
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from common.annotation.log_annotation import Log
|
||||||
|
from common.aspect.db_seesion import DBSessionDependency
|
||||||
|
from common.aspect.interface_auth import UserInterfaceAuthDependency
|
||||||
|
from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
|
||||||
|
from common.enums import BusinessType
|
||||||
|
from common.router import APIRouterPro
|
||||||
|
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
|
||||||
|
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||||
|
from module_business.entity.vo.operation_vo import (
|
||||||
|
BusinessOptionModel,
|
||||||
|
ConfirmCommandModel,
|
||||||
|
InboundCreateModel,
|
||||||
|
InboundModel,
|
||||||
|
InboundPageQueryModel,
|
||||||
|
InboundScanQueryModel,
|
||||||
|
InboundUpdateModel,
|
||||||
|
PalletScanResponseModel,
|
||||||
|
)
|
||||||
|
from module_business.service.inbound_service import InboundService
|
||||||
|
from utils.response_util import ResponseUtil
|
||||||
|
|
||||||
|
inbound_controller = APIRouterPro(
|
||||||
|
prefix='/business/inbound',
|
||||||
|
tags=['业务管理-仓库入库'],
|
||||||
|
dependencies=[PreAuthDependency()],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.get(
|
||||||
|
'/list',
|
||||||
|
summary='获取首次入库单分页列表接口',
|
||||||
|
description='按当前用户仓库数据范围获取首次入库单分页列表',
|
||||||
|
response_model=PageResponseModel[InboundModel],
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:list')],
|
||||||
|
)
|
||||||
|
async def get_business_inbound_list(
|
||||||
|
request: Request,
|
||||||
|
inbound_page_query: Annotated[InboundPageQueryModel, Query()],
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.get_inbound_list_services(
|
||||||
|
query_db,
|
||||||
|
inbound_page_query,
|
||||||
|
current_user,
|
||||||
|
is_page=True,
|
||||||
|
)
|
||||||
|
return ResponseUtil.success(model_content=result)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.get(
|
||||||
|
'/warehouse-options',
|
||||||
|
summary='获取首次入库仓库选项接口',
|
||||||
|
description='仅返回当前用户有权访问的启用仓库',
|
||||||
|
response_model=DataResponseModel[list[BusinessOptionModel]],
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:query')],
|
||||||
|
)
|
||||||
|
async def get_business_inbound_warehouse_options(
|
||||||
|
request: Request,
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.get_warehouse_options_services(query_db, current_user)
|
||||||
|
return ResponseUtil.success(data=result)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.get(
|
||||||
|
'/scan/{qr_token}',
|
||||||
|
summary='扫描首次入库托盘接口',
|
||||||
|
description='查询托盘是否可加入所选仓库的首次入库单',
|
||||||
|
response_model=DataResponseModel[PalletScanResponseModel],
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:query')],
|
||||||
|
)
|
||||||
|
async def scan_business_inbound_pallet(
|
||||||
|
request: Request,
|
||||||
|
qr_token: Annotated[
|
||||||
|
str,
|
||||||
|
Path(min_length=1, max_length=128, pattern=r'\S', description='二维码固定标识'),
|
||||||
|
],
|
||||||
|
inbound_scan_query: Annotated[InboundScanQueryModel, Query()],
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.scan_inbound_pallet_services(
|
||||||
|
query_db,
|
||||||
|
qr_token,
|
||||||
|
inbound_scan_query,
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
|
return ResponseUtil.success(data=result)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.get(
|
||||||
|
'/{inbound_id}',
|
||||||
|
summary='获取首次入库单详情接口',
|
||||||
|
description='按仓库数据范围获取指定首次入库单详情',
|
||||||
|
response_model=DataResponseModel[InboundModel],
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:query')],
|
||||||
|
)
|
||||||
|
async def query_detail_business_inbound(
|
||||||
|
request: Request,
|
||||||
|
inbound_id: Annotated[int, Path(gt=0, description='入库单ID')],
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.inbound_detail_services(query_db, inbound_id, current_user)
|
||||||
|
return ResponseUtil.success(data=result)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.post(
|
||||||
|
'',
|
||||||
|
summary='新增首次入库草稿接口',
|
||||||
|
description='新增空托盘首次入库草稿',
|
||||||
|
response_model=ResponseBaseModel,
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:add')],
|
||||||
|
)
|
||||||
|
@Log(title='仓库入库', business_type=BusinessType.INSERT)
|
||||||
|
async def add_business_inbound(
|
||||||
|
request: Request,
|
||||||
|
add_inbound: InboundCreateModel,
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.add_inbound_services(query_db, add_inbound, current_user)
|
||||||
|
return ResponseUtil.success(msg=result.message)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.put(
|
||||||
|
'',
|
||||||
|
summary='编辑首次入库草稿接口',
|
||||||
|
description='编辑尚未确认的首次入库草稿',
|
||||||
|
response_model=ResponseBaseModel,
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:edit')],
|
||||||
|
)
|
||||||
|
@Log(title='仓库入库', business_type=BusinessType.UPDATE)
|
||||||
|
async def edit_business_inbound(
|
||||||
|
request: Request,
|
||||||
|
edit_inbound: InboundUpdateModel,
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.edit_inbound_services(query_db, edit_inbound, current_user)
|
||||||
|
return ResponseUtil.success(msg=result.message)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.delete(
|
||||||
|
'/{inbound_ids}',
|
||||||
|
summary='删除首次入库草稿接口',
|
||||||
|
description='原子删除一个或多个尚未确认的首次入库草稿',
|
||||||
|
response_model=ResponseBaseModel,
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:remove')],
|
||||||
|
)
|
||||||
|
@Log(title='仓库入库', business_type=BusinessType.DELETE)
|
||||||
|
async def delete_business_inbound(
|
||||||
|
request: Request,
|
||||||
|
inbound_ids: Annotated[
|
||||||
|
str,
|
||||||
|
Path(pattern=r'^[1-9]\d*(,[1-9]\d*)*$', description='需要删除的入库单ID'),
|
||||||
|
],
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.delete_inbound_services(
|
||||||
|
query_db,
|
||||||
|
[int(inbound_id) for inbound_id in inbound_ids.split(',')],
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
|
return ResponseUtil.success(msg=result.message)
|
||||||
|
|
||||||
|
|
||||||
|
@inbound_controller.put(
|
||||||
|
'/{inbound_id}/confirm',
|
||||||
|
summary='确认首次入库单接口',
|
||||||
|
description='原子确认整张首次入库单并追加托盘流转事件',
|
||||||
|
response_model=ResponseBaseModel,
|
||||||
|
dependencies=[UserInterfaceAuthDependency('business:inbound:confirm')],
|
||||||
|
)
|
||||||
|
@Log(title='仓库入库', business_type=BusinessType.UPDATE)
|
||||||
|
async def confirm_business_inbound(
|
||||||
|
request: Request,
|
||||||
|
inbound_id: Annotated[int, Path(gt=0, description='入库单ID')],
|
||||||
|
confirm_command: ConfirmCommandModel,
|
||||||
|
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||||
|
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||||
|
) -> Response:
|
||||||
|
result = await InboundService.confirm_inbound_services(
|
||||||
|
query_db,
|
||||||
|
inbound_id,
|
||||||
|
confirm_command,
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
|
return ResponseUtil.success(msg=result.message)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import ColumnElement, Select, delete, select, update
|
from sqlalchemy import ColumnElement, Select, delete, false, select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||||
|
|
||||||
@@ -186,12 +186,28 @@ class OperationDao:
|
|||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
inbound_id: int,
|
inbound_id: int,
|
||||||
) -> BizInboundOrder | None:
|
) -> BizInboundOrder | None:
|
||||||
return await cls._get_for_update(
|
inbound = await cls._get_for_update(
|
||||||
db,
|
db,
|
||||||
BizInboundOrder,
|
BizInboundOrder,
|
||||||
BizInboundOrder.inbound_id,
|
BizInboundOrder.inbound_id,
|
||||||
inbound_id,
|
inbound_id,
|
||||||
)
|
)
|
||||||
|
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
|
@classmethod
|
||||||
async def get_shipment_for_update(
|
async def get_shipment_for_update(
|
||||||
@@ -318,8 +334,14 @@ class OperationDao:
|
|||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
query_object: InboundPageQueryModel,
|
query_object: InboundPageQueryModel,
|
||||||
is_page: bool = True,
|
is_page: bool = True,
|
||||||
|
warehouse_ids: list[int] | None = None,
|
||||||
) -> PageModel | list[dict[str, Any]]:
|
) -> PageModel | list[dict[str, Any]]:
|
||||||
conditions = []
|
conditions = []
|
||||||
|
if warehouse_ids is not None:
|
||||||
|
scoped_warehouse_ids = sorted(set(warehouse_ids))
|
||||||
|
conditions.append(
|
||||||
|
BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()
|
||||||
|
)
|
||||||
if query_object.inbound_no:
|
if query_object.inbound_no:
|
||||||
conditions.append(BizInboundOrder.inbound_no.like(f'%{query_object.inbound_no}%'))
|
conditions.append(BizInboundOrder.inbound_no.like(f'%{query_object.inbound_no}%'))
|
||||||
if query_object.warehouse_id:
|
if query_object.warehouse_id:
|
||||||
|
|||||||
@@ -0,0 +1,579 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from common.vo import CrudResponseModel, PageModel
|
||||||
|
from exceptions.exception import PermissionException, ServiceException
|
||||||
|
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||||
|
from module_business.dao.access_dao import AccessDao
|
||||||
|
from module_business.dao.inventory_dao import InventoryDao
|
||||||
|
from module_business.dao.master_dao import MasterDao
|
||||||
|
from module_business.dao.operation_dao import OperationDao
|
||||||
|
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
|
||||||
|
from module_business.domain.pallet_state_machine import PalletStateMachine
|
||||||
|
from module_business.entity.do.master_do import BizPallet
|
||||||
|
from module_business.entity.do.operation_do import (
|
||||||
|
BizInboundItem,
|
||||||
|
BizInboundOrder,
|
||||||
|
BizPalletEvent,
|
||||||
|
BizPalletInventory,
|
||||||
|
)
|
||||||
|
from module_business.entity.vo.master_vo import WarehousePageQueryModel
|
||||||
|
from module_business.entity.vo.operation_vo import (
|
||||||
|
BusinessOptionModel,
|
||||||
|
ConfirmCommandModel,
|
||||||
|
InboundCreateModel,
|
||||||
|
InboundModel,
|
||||||
|
InboundPageQueryModel,
|
||||||
|
InboundScanQueryModel,
|
||||||
|
InboundUpdateModel,
|
||||||
|
PalletScanResponseModel,
|
||||||
|
)
|
||||||
|
from module_business.service.access_service import AccessService
|
||||||
|
|
||||||
|
|
||||||
|
class InboundService:
|
||||||
|
"""空托盘首次入库服务层。"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _current_identity(current_user: CurrentUserModel) -> tuple[int, str]:
|
||||||
|
user = current_user.user
|
||||||
|
if user is None or user.user_id is None or user.user_id <= 0 or not user.user_name:
|
||||||
|
raise ServiceException(message='当前用户信息无效')
|
||||||
|
return user.user_id, user.user_name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_admin(current_user: CurrentUserModel) -> bool:
|
||||||
|
return bool(current_user.user and current_user.user.admin)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _record_value(record: Any, field: str) -> Any:
|
||||||
|
if not isinstance(record, dict):
|
||||||
|
return getattr(record, field)
|
||||||
|
if field in record:
|
||||||
|
return record[field]
|
||||||
|
words = field.split('_')
|
||||||
|
alias = words[0] + ''.join(word.capitalize() for word in words[1:])
|
||||||
|
return record.get(alias)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_confirm_request_hash(inbound_id: int) -> str:
|
||||||
|
payload = json.dumps(
|
||||||
|
{'inbound_id': inbound_id},
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(',', ':'),
|
||||||
|
).encode()
|
||||||
|
return hashlib.sha256(payload).hexdigest()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _warehouse_scope(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> list[int] | None:
|
||||||
|
if cls._is_admin(current_user):
|
||||||
|
return None
|
||||||
|
user_id, _ = cls._current_identity(current_user)
|
||||||
|
return sorted(set(await AccessDao.list_user_warehouse_ids(query_db, user_id)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _validate_active_warehouse(
|
||||||
|
query_db: AsyncSession,
|
||||||
|
warehouse_id: int,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> Any:
|
||||||
|
warehouse = await MasterDao.get_warehouse(query_db, warehouse_id)
|
||||||
|
if warehouse is None:
|
||||||
|
raise ServiceException(message='目标仓库不存在')
|
||||||
|
if warehouse.status != '0':
|
||||||
|
raise ServiceException(message='目标仓库已停用')
|
||||||
|
await AccessService.assert_warehouse_access(query_db, current_user, warehouse_id)
|
||||||
|
return warehouse
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_pallet_pair(pallet: BizPallet, inventory: BizPalletInventory) -> None:
|
||||||
|
if pallet.asset_status != '0':
|
||||||
|
raise ServiceException(message='托盘资产已停用,不允许首次入库')
|
||||||
|
if (
|
||||||
|
inventory.flow_state != PalletFlowState.UNSTOCKED.value
|
||||||
|
or inventory.warehouse_id is not None
|
||||||
|
or inventory.site_id is not None
|
||||||
|
or inventory.current_line_id is not None
|
||||||
|
or inventory.active_filling_id is not None
|
||||||
|
or inventory.remaining_packages != 0
|
||||||
|
):
|
||||||
|
raise ServiceException(message='托盘不符合首次入库条件')
|
||||||
|
if not isinstance(inventory.version, int) or inventory.version < 0:
|
||||||
|
raise ServiceException(message='托盘库存版本无效')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _lock_and_validate_pallets(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
pallet_ids: list[int],
|
||||||
|
) -> tuple[list[BizPalletInventory], list[BizPallet]]:
|
||||||
|
ordered_ids = sorted(pallet_ids)
|
||||||
|
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids)
|
||||||
|
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids)
|
||||||
|
expected_ids = set(ordered_ids)
|
||||||
|
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
|
||||||
|
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
|
||||||
|
if set(inventory_by_id) != expected_ids or set(pallet_by_id) != expected_ids:
|
||||||
|
raise ServiceException(message='存在不存在或库存缺失的托盘')
|
||||||
|
|
||||||
|
ordered_inventories = [inventory_by_id[pallet_id] for pallet_id in ordered_ids]
|
||||||
|
ordered_pallets = [pallet_by_id[pallet_id] for pallet_id in ordered_ids]
|
||||||
|
for pallet, inventory in zip(ordered_pallets, ordered_inventories, strict=True):
|
||||||
|
cls._validate_pallet_pair(pallet, inventory)
|
||||||
|
return ordered_inventories, ordered_pallets
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_items(
|
||||||
|
inbound_id: int,
|
||||||
|
pallet_ids: list[int],
|
||||||
|
user_name: str,
|
||||||
|
current_time: datetime,
|
||||||
|
) -> list[BizInboundItem]:
|
||||||
|
return [
|
||||||
|
BizInboundItem(
|
||||||
|
inbound_id=inbound_id,
|
||||||
|
pallet_id=pallet_id,
|
||||||
|
create_by=user_name,
|
||||||
|
create_time=current_time,
|
||||||
|
)
|
||||||
|
for pallet_id in sorted(pallet_ids)
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _success(message: str) -> CrudResponseModel:
|
||||||
|
return CrudResponseModel(is_success=True, message=message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_inbound_list_services(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
query_object: InboundPageQueryModel,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
is_page: bool = True,
|
||||||
|
) -> PageModel | list[InboundModel]:
|
||||||
|
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
|
||||||
|
if (
|
||||||
|
warehouse_ids is not None
|
||||||
|
and query_object.warehouse_id is not None
|
||||||
|
and query_object.warehouse_id not in warehouse_ids
|
||||||
|
):
|
||||||
|
raise PermissionException(data='', message='无权访问该仓库')
|
||||||
|
|
||||||
|
result = await OperationDao.get_inbound_list(
|
||||||
|
query_db,
|
||||||
|
query_object,
|
||||||
|
is_page,
|
||||||
|
warehouse_ids=warehouse_ids,
|
||||||
|
)
|
||||||
|
rows = result.rows if isinstance(result, PageModel) else result
|
||||||
|
models = [InboundModel.model_validate(record) for record in rows]
|
||||||
|
if isinstance(result, PageModel):
|
||||||
|
return result.model_copy(update={'rows': models})
|
||||||
|
return models
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def inbound_detail_services(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
inbound_id: int,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> InboundModel:
|
||||||
|
inbound = await OperationDao.get_inbound_by_id(query_db, inbound_id)
|
||||||
|
if inbound is None:
|
||||||
|
raise ServiceException(message='入库单不存在')
|
||||||
|
await AccessService.assert_warehouse_access(query_db, current_user, inbound.warehouse_id)
|
||||||
|
return InboundModel.model_validate(inbound)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_warehouse_options_services(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> list[BusinessOptionModel]:
|
||||||
|
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
|
||||||
|
allowed_ids = None if warehouse_ids is None else set(warehouse_ids)
|
||||||
|
warehouses = await MasterDao.list_warehouse(
|
||||||
|
query_db,
|
||||||
|
WarehousePageQueryModel(status='0'),
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
options = []
|
||||||
|
for warehouse in warehouses:
|
||||||
|
warehouse_id = cls._record_value(warehouse, 'warehouse_id')
|
||||||
|
if cls._record_value(warehouse, 'status') != '0':
|
||||||
|
continue
|
||||||
|
if allowed_ids is not None and warehouse_id not in allowed_ids:
|
||||||
|
continue
|
||||||
|
options.append(
|
||||||
|
BusinessOptionModel(
|
||||||
|
id=warehouse_id,
|
||||||
|
code=cls._record_value(warehouse, 'warehouse_code'),
|
||||||
|
name=cls._record_value(warehouse, 'warehouse_name'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorted(options, key=lambda option: option.id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def scan_inbound_pallet_services(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
qr_token: str,
|
||||||
|
query_object: InboundScanQueryModel,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> PalletScanResponseModel:
|
||||||
|
await AccessService.assert_warehouse_access(
|
||||||
|
query_db,
|
||||||
|
current_user,
|
||||||
|
query_object.warehouse_id,
|
||||||
|
)
|
||||||
|
warehouse = await MasterDao.get_warehouse(query_db, query_object.warehouse_id)
|
||||||
|
if warehouse is None:
|
||||||
|
raise ServiceException(message='目标仓库不存在')
|
||||||
|
if warehouse.status != '0':
|
||||||
|
raise ServiceException(message='目标仓库已停用')
|
||||||
|
|
||||||
|
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token)
|
||||||
|
if pallet is None:
|
||||||
|
raise ServiceException(message='托盘不存在')
|
||||||
|
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id)
|
||||||
|
if inventory is None:
|
||||||
|
raise ServiceException(message='托盘库存不存在')
|
||||||
|
cls._validate_pallet_pair(pallet, inventory)
|
||||||
|
return PalletScanResponseModel(
|
||||||
|
pallet_id=pallet.pallet_id,
|
||||||
|
pallet_code=pallet.pallet_code,
|
||||||
|
flow_state=PalletFlowState.UNSTOCKED,
|
||||||
|
warehouse_id=None,
|
||||||
|
site_id=None,
|
||||||
|
current_line_id=None,
|
||||||
|
active_filling_id=None,
|
||||||
|
strain_id=None,
|
||||||
|
strain_code=None,
|
||||||
|
strain_name=None,
|
||||||
|
batch_no=None,
|
||||||
|
package_spec=None,
|
||||||
|
capacity_packages=pallet.capacity_packages,
|
||||||
|
remaining_packages=0,
|
||||||
|
allowed_actions=['inbound'],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def add_inbound_services(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
page_object: InboundCreateModel,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> CrudResponseModel:
|
||||||
|
try:
|
||||||
|
_, user_name = cls._current_identity(current_user)
|
||||||
|
await cls._validate_active_warehouse(
|
||||||
|
query_db,
|
||||||
|
page_object.warehouse_id,
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
|
ordered_ids = sorted(page_object.pallet_ids)
|
||||||
|
await cls._lock_and_validate_pallets(query_db, ordered_ids)
|
||||||
|
|
||||||
|
current_time = datetime.now()
|
||||||
|
inbound = BizInboundOrder(
|
||||||
|
inbound_no=f'IN-{secrets.token_hex(16)}',
|
||||||
|
warehouse_id=page_object.warehouse_id,
|
||||||
|
status=DocumentStatus.DRAFT.value,
|
||||||
|
create_by=user_name,
|
||||||
|
create_time=current_time,
|
||||||
|
update_by=user_name,
|
||||||
|
update_time=current_time,
|
||||||
|
)
|
||||||
|
inbound = await OperationDao.add_inbound(query_db, inbound)
|
||||||
|
await OperationDao.replace_inbound_items(
|
||||||
|
query_db,
|
||||||
|
inbound.inbound_id,
|
||||||
|
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time),
|
||||||
|
)
|
||||||
|
await query_db.commit()
|
||||||
|
return cls._success('新增成功')
|
||||||
|
except IntegrityError:
|
||||||
|
await query_db.rollback()
|
||||||
|
raise ServiceException(message='新增入库单失败,入库单号或明细已存在') from None
|
||||||
|
except Exception:
|
||||||
|
await query_db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def edit_inbound_services(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
page_object: InboundUpdateModel,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> CrudResponseModel:
|
||||||
|
try:
|
||||||
|
_, user_name = cls._current_identity(current_user)
|
||||||
|
inbound = await OperationDao.get_inbound_for_update(query_db, page_object.inbound_id)
|
||||||
|
if inbound is None:
|
||||||
|
raise ServiceException(message='入库单不存在')
|
||||||
|
await AccessService.assert_warehouse_access(
|
||||||
|
query_db,
|
||||||
|
current_user,
|
||||||
|
inbound.warehouse_id,
|
||||||
|
)
|
||||||
|
if inbound.status != DocumentStatus.DRAFT.value:
|
||||||
|
raise ServiceException(message='已确认入库单不允许编辑')
|
||||||
|
await cls._validate_active_warehouse(
|
||||||
|
query_db,
|
||||||
|
page_object.warehouse_id,
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
|
ordered_ids = sorted(page_object.pallet_ids)
|
||||||
|
await cls._lock_and_validate_pallets(query_db, ordered_ids)
|
||||||
|
|
||||||
|
current_time = datetime.now()
|
||||||
|
inbound.warehouse_id = page_object.warehouse_id
|
||||||
|
inbound.update_by = user_name
|
||||||
|
inbound.update_time = current_time
|
||||||
|
await OperationDao.update_inbound(query_db, inbound)
|
||||||
|
await OperationDao.replace_inbound_items(
|
||||||
|
query_db,
|
||||||
|
inbound.inbound_id,
|
||||||
|
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time),
|
||||||
|
)
|
||||||
|
await query_db.commit()
|
||||||
|
return cls._success('修改成功')
|
||||||
|
except IntegrityError:
|
||||||
|
await query_db.rollback()
|
||||||
|
raise ServiceException(message='修改入库单失败,单据或明细状态已变化') from None
|
||||||
|
except Exception:
|
||||||
|
await query_db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_inbound_services(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
inbound_ids: list[int],
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> CrudResponseModel:
|
||||||
|
try:
|
||||||
|
if not inbound_ids or any(inbound_id <= 0 for inbound_id in inbound_ids):
|
||||||
|
raise ServiceException(message='入库单ID必须为正整数')
|
||||||
|
if len(inbound_ids) != len(set(inbound_ids)):
|
||||||
|
raise ServiceException(message='入库单ID不能重复')
|
||||||
|
|
||||||
|
ordered_ids = sorted(inbound_ids)
|
||||||
|
for inbound_id in ordered_ids:
|
||||||
|
inbound = await OperationDao.get_inbound_for_update(query_db, inbound_id)
|
||||||
|
if inbound is None:
|
||||||
|
raise ServiceException(message='入库单不存在')
|
||||||
|
await AccessService.assert_warehouse_access(
|
||||||
|
query_db,
|
||||||
|
current_user,
|
||||||
|
inbound.warehouse_id,
|
||||||
|
)
|
||||||
|
if inbound.status != DocumentStatus.DRAFT.value:
|
||||||
|
raise ServiceException(message='已确认入库单不允许删除')
|
||||||
|
|
||||||
|
for inbound_id in ordered_ids:
|
||||||
|
await OperationDao.delete_inbound(query_db, inbound_id)
|
||||||
|
await query_db.commit()
|
||||||
|
return cls._success('删除成功')
|
||||||
|
except IntegrityError:
|
||||||
|
await query_db.rollback()
|
||||||
|
raise ServiceException(message='删除入库单失败,单据状态或关联数据已变化') from None
|
||||||
|
except Exception:
|
||||||
|
await query_db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_exact_confirmed(
|
||||||
|
inbound: BizInboundOrder,
|
||||||
|
inbound_id: int,
|
||||||
|
idempotency_key: str,
|
||||||
|
request_hash: str,
|
||||||
|
) -> bool:
|
||||||
|
return (
|
||||||
|
inbound.inbound_id == inbound_id
|
||||||
|
and inbound.status == DocumentStatus.CONFIRMED.value
|
||||||
|
and inbound.confirm_idempotency_key == idempotency_key
|
||||||
|
and inbound.confirm_request_hash == request_hash
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _recover_confirm_integrity_error(
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
inbound_id: int,
|
||||||
|
idempotency_key: str,
|
||||||
|
request_hash: str,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> CrudResponseModel:
|
||||||
|
try:
|
||||||
|
recovered = await OperationDao.get_inbound_by_confirm_idempotency_key(
|
||||||
|
query_db,
|
||||||
|
idempotency_key,
|
||||||
|
)
|
||||||
|
if recovered is None:
|
||||||
|
raise ServiceException(message='确认入库失败,幂等冲突')
|
||||||
|
await AccessService.assert_warehouse_access(
|
||||||
|
query_db,
|
||||||
|
current_user,
|
||||||
|
recovered.warehouse_id,
|
||||||
|
)
|
||||||
|
if not cls._is_exact_confirmed(
|
||||||
|
recovered,
|
||||||
|
inbound_id,
|
||||||
|
idempotency_key,
|
||||||
|
request_hash,
|
||||||
|
):
|
||||||
|
raise ServiceException(message='确认入库失败,幂等键已被其他请求使用')
|
||||||
|
return cls._success('确认成功')
|
||||||
|
finally:
|
||||||
|
await query_db.rollback()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def confirm_inbound_services( # noqa: PLR0912, PLR0915
|
||||||
|
cls,
|
||||||
|
query_db: AsyncSession,
|
||||||
|
inbound_id: int,
|
||||||
|
page_object: ConfirmCommandModel,
|
||||||
|
current_user: CurrentUserModel,
|
||||||
|
) -> CrudResponseModel:
|
||||||
|
try:
|
||||||
|
user_id, user_name = cls._current_identity(current_user)
|
||||||
|
inbound = await OperationDao.get_inbound_by_id(query_db, inbound_id)
|
||||||
|
if inbound is None:
|
||||||
|
raise ServiceException(message='入库单不存在')
|
||||||
|
await AccessService.assert_warehouse_access(
|
||||||
|
query_db,
|
||||||
|
current_user,
|
||||||
|
inbound.warehouse_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
request_hash = cls._build_confirm_request_hash(inbound_id)
|
||||||
|
idempotency_key = page_object.idempotency_key
|
||||||
|
existing = await OperationDao.get_inbound_by_confirm_idempotency_key(
|
||||||
|
query_db,
|
||||||
|
idempotency_key,
|
||||||
|
)
|
||||||
|
if existing is not None:
|
||||||
|
await AccessService.assert_warehouse_access(
|
||||||
|
query_db,
|
||||||
|
current_user,
|
||||||
|
existing.warehouse_id,
|
||||||
|
)
|
||||||
|
if not cls._is_exact_confirmed(
|
||||||
|
existing,
|
||||||
|
inbound_id,
|
||||||
|
idempotency_key,
|
||||||
|
request_hash,
|
||||||
|
):
|
||||||
|
raise ServiceException(message='确认入库失败,幂等键已被其他请求使用')
|
||||||
|
|
||||||
|
locked_inbound = await OperationDao.get_inbound_for_update(query_db, inbound_id)
|
||||||
|
if locked_inbound is None:
|
||||||
|
raise ServiceException(message='入库单不存在')
|
||||||
|
await AccessService.assert_warehouse_access(
|
||||||
|
query_db,
|
||||||
|
current_user,
|
||||||
|
locked_inbound.warehouse_id,
|
||||||
|
)
|
||||||
|
if locked_inbound.status == DocumentStatus.CONFIRMED.value:
|
||||||
|
if cls._is_exact_confirmed(
|
||||||
|
locked_inbound,
|
||||||
|
inbound_id,
|
||||||
|
idempotency_key,
|
||||||
|
request_hash,
|
||||||
|
):
|
||||||
|
await query_db.rollback()
|
||||||
|
return cls._success('确认成功')
|
||||||
|
raise ServiceException(message='入库单已确认,幂等信息不匹配')
|
||||||
|
if locked_inbound.status != DocumentStatus.DRAFT.value:
|
||||||
|
raise ServiceException(message='当前入库单状态不允许确认')
|
||||||
|
if existing is not None:
|
||||||
|
raise ServiceException(message='确认入库失败,并发状态不一致')
|
||||||
|
if locked_inbound.confirm_idempotency_key is not None or locked_inbound.confirm_request_hash is not None:
|
||||||
|
raise ServiceException(message='入库单确认信息不完整')
|
||||||
|
|
||||||
|
pallet_ids = [item.pallet_id for item in locked_inbound.items]
|
||||||
|
if not pallet_ids:
|
||||||
|
raise ServiceException(message='入库单至少需要一个托盘')
|
||||||
|
if len(pallet_ids) != len(set(pallet_ids)):
|
||||||
|
raise ServiceException(message='入库单托盘不能重复')
|
||||||
|
ordered_ids = sorted(pallet_ids)
|
||||||
|
inventories, _ = await cls._lock_and_validate_pallets(query_db, ordered_ids)
|
||||||
|
|
||||||
|
for inventory in inventories:
|
||||||
|
PalletStateMachine.ensure_transition(
|
||||||
|
PalletFlowState(inventory.flow_state),
|
||||||
|
PalletFlowState.EMPTY_IN_WAREHOUSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
current_time = datetime.now()
|
||||||
|
locked_inbound.status = DocumentStatus.CONFIRMED.value
|
||||||
|
locked_inbound.confirm_idempotency_key = idempotency_key
|
||||||
|
locked_inbound.confirm_request_hash = request_hash
|
||||||
|
locked_inbound.confirmed_by = user_id
|
||||||
|
locked_inbound.confirmed_at = current_time
|
||||||
|
locked_inbound.update_by = user_name
|
||||||
|
locked_inbound.update_time = current_time
|
||||||
|
await OperationDao.update_inbound(query_db, locked_inbound)
|
||||||
|
|
||||||
|
for inventory in inventories:
|
||||||
|
before_state = inventory.flow_state
|
||||||
|
before_warehouse_id = inventory.warehouse_id
|
||||||
|
before_site_id = inventory.site_id
|
||||||
|
before_line_id = inventory.current_line_id
|
||||||
|
before_remaining_packages = inventory.remaining_packages
|
||||||
|
|
||||||
|
inventory.flow_state = PalletFlowState.EMPTY_IN_WAREHOUSE.value
|
||||||
|
inventory.warehouse_id = locked_inbound.warehouse_id
|
||||||
|
inventory.site_id = None
|
||||||
|
inventory.current_line_id = None
|
||||||
|
inventory.active_filling_id = None
|
||||||
|
inventory.remaining_packages = 0
|
||||||
|
inventory.version += 1
|
||||||
|
inventory.updated_at = current_time
|
||||||
|
event = BizPalletEvent(
|
||||||
|
pallet_id=inventory.pallet_id,
|
||||||
|
event_type=PalletEventType.INBOUND_CONFIRMED.value,
|
||||||
|
source_type='INBOUND',
|
||||||
|
source_id=inbound_id,
|
||||||
|
before_state=before_state,
|
||||||
|
after_state=PalletFlowState.EMPTY_IN_WAREHOUSE.value,
|
||||||
|
before_warehouse_id=before_warehouse_id,
|
||||||
|
before_site_id=before_site_id,
|
||||||
|
before_line_id=before_line_id,
|
||||||
|
after_warehouse_id=locked_inbound.warehouse_id,
|
||||||
|
after_site_id=None,
|
||||||
|
after_line_id=None,
|
||||||
|
before_remaining_packages=before_remaining_packages,
|
||||||
|
after_remaining_packages=0,
|
||||||
|
operator_id=user_id,
|
||||||
|
occurred_at=current_time,
|
||||||
|
idempotency_key=idempotency_key,
|
||||||
|
request_hash=request_hash,
|
||||||
|
)
|
||||||
|
await InventoryDao.add_event(query_db, inventory, event)
|
||||||
|
|
||||||
|
await query_db.commit()
|
||||||
|
return cls._success('确认成功')
|
||||||
|
except IntegrityError:
|
||||||
|
await query_db.rollback()
|
||||||
|
return await cls._recover_confirm_integrity_error(
|
||||||
|
query_db,
|
||||||
|
inbound_id,
|
||||||
|
page_object.idempotency_key,
|
||||||
|
cls._build_confirm_request_hash(inbound_id),
|
||||||
|
current_user,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
await query_db.rollback()
|
||||||
|
raise
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -156,8 +156,15 @@ class TestDaoPublicContract(unittest.TestCase):
|
|||||||
signature = inspect.signature(getattr(OperationDao, method_name))
|
signature = inspect.signature(getattr(OperationDao, method_name))
|
||||||
self.assertEqual(parameter_names, tuple(signature.parameters))
|
self.assertEqual(parameter_names, tuple(signature.parameters))
|
||||||
|
|
||||||
|
inbound_list_signature = inspect.signature(OperationDao.get_inbound_list)
|
||||||
|
self.assertEqual(
|
||||||
|
('db', 'query_object', 'is_page', 'warehouse_ids'),
|
||||||
|
tuple(inbound_list_signature.parameters),
|
||||||
|
)
|
||||||
|
self.assertIs(True, inbound_list_signature.parameters['is_page'].default)
|
||||||
|
self.assertIsNone(inbound_list_signature.parameters['warehouse_ids'].default)
|
||||||
|
|
||||||
for method_name in (
|
for method_name in (
|
||||||
'get_inbound_list',
|
|
||||||
'get_filling_list',
|
'get_filling_list',
|
||||||
'get_shipment_list',
|
'get_shipment_list',
|
||||||
'get_usage_list',
|
'get_usage_list',
|
||||||
@@ -360,13 +367,6 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_for_update_queries_target_primary_keys_and_lock_rows(self) -> None:
|
async def test_for_update_queries_target_primary_keys_and_lock_rows(self) -> None:
|
||||||
cases = (
|
cases = (
|
||||||
(
|
|
||||||
OperationDao.get_inbound_for_update,
|
|
||||||
BizInboundOrder,
|
|
||||||
'inbound_id',
|
|
||||||
11,
|
|
||||||
(),
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
OperationDao.get_shipment_for_update,
|
OperationDao.get_shipment_for_update,
|
||||||
BizShipmentOrder,
|
BizShipmentOrder,
|
||||||
@@ -410,6 +410,42 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
|
|||||||
for fragment in extra_fragments:
|
for fragment in extra_fragments:
|
||||||
self.assertIn(fragment, sql)
|
self.assertIn(fragment, sql)
|
||||||
|
|
||||||
|
async def test_inbound_for_update_locks_parent_then_current_items(self) -> None:
|
||||||
|
inbound = SimpleNamespace(inbound_id=11)
|
||||||
|
items = [
|
||||||
|
SimpleNamespace(inbound_item_id=2, inbound_id=11, pallet_id=2),
|
||||||
|
SimpleNamespace(inbound_item_id=5, inbound_id=11, pallet_id=5),
|
||||||
|
]
|
||||||
|
inbound_result = MagicMock()
|
||||||
|
inbound_result.scalar_one_or_none.return_value = inbound
|
||||||
|
item_result = MagicMock()
|
||||||
|
item_result.scalars.return_value.all.return_value = items
|
||||||
|
self.db.execute.side_effect = [inbound_result, item_result]
|
||||||
|
|
||||||
|
actual = await OperationDao.get_inbound_for_update(self.db, inbound_id=11)
|
||||||
|
|
||||||
|
self.assertIs(inbound, actual)
|
||||||
|
self.assertEqual(items, actual.items)
|
||||||
|
self.assertEqual(2, self.db.execute.await_count)
|
||||||
|
parent_statement = self.db.execute.await_args_list[0].args[0]
|
||||||
|
item_statement = self.db.execute.await_args_list[1].args[0]
|
||||||
|
self.assertTrue(parent_statement.get_final_froms()[0].compare(BizInboundOrder.__table__))
|
||||||
|
self.assertTrue(item_statement.get_final_froms()[0].compare(BizInboundItem.__table__))
|
||||||
|
self.assertIs(True, parent_statement.get_execution_options().get('populate_existing'))
|
||||||
|
self.assertIs(True, item_statement.get_execution_options().get('populate_existing'))
|
||||||
|
for dialect in (mysql.dialect(), postgresql.dialect()):
|
||||||
|
with self.subTest(dialect=dialect.name):
|
||||||
|
parent_sql = compile_sql(parent_statement, dialect)
|
||||||
|
item_sql = compile_sql(item_statement, dialect)
|
||||||
|
self.assertIn('biz_inbound_order.inbound_id = 11', parent_sql)
|
||||||
|
self.assertIn('FOR UPDATE', parent_sql)
|
||||||
|
self.assertIn('biz_inbound_item.inbound_id = 11', item_sql)
|
||||||
|
self.assertIn(
|
||||||
|
'ORDER BY biz_inbound_item.pallet_id, biz_inbound_item.inbound_item_id',
|
||||||
|
item_sql,
|
||||||
|
)
|
||||||
|
self.assertIn('FOR UPDATE', item_sql)
|
||||||
|
|
||||||
async def test_idempotency_queries_use_the_exact_declared_column(self) -> None:
|
async def test_idempotency_queries_use_the_exact_declared_column(self) -> None:
|
||||||
cases = (
|
cases = (
|
||||||
(
|
(
|
||||||
@@ -588,6 +624,58 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
|
|||||||
for fragment in fragments:
|
for fragment in fragments:
|
||||||
self.assertIn(fragment, sql)
|
self.assertIn(fragment, sql)
|
||||||
|
|
||||||
|
async def test_inbound_list_applies_warehouse_scope_inside_paginated_statement(self) -> None:
|
||||||
|
query = InboundPageQueryModel(page_num=2, page_size=7)
|
||||||
|
marker = object()
|
||||||
|
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as paginate:
|
||||||
|
actual = await OperationDao.get_inbound_list(
|
||||||
|
self.db,
|
||||||
|
query,
|
||||||
|
is_page=True,
|
||||||
|
warehouse_ids=[5, 2, 5],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(marker, actual)
|
||||||
|
statement = paginate.await_args.args[1]
|
||||||
|
for dialect in (mysql.dialect(), postgresql.dialect()):
|
||||||
|
with self.subTest(dialect=dialect.name):
|
||||||
|
sql = compile_sql(statement, dialect)
|
||||||
|
self.assertIn('biz_inbound_order.warehouse_id IN (2, 5)', sql)
|
||||||
|
self.assertIn('ORDER BY biz_inbound_order.inbound_id', sql)
|
||||||
|
|
||||||
|
async def test_inbound_list_empty_scope_compiles_to_false_for_both_dialects(self) -> None:
|
||||||
|
query = InboundPageQueryModel(page_num=4, page_size=6)
|
||||||
|
marker = object()
|
||||||
|
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as paginate:
|
||||||
|
actual = await OperationDao.get_inbound_list(
|
||||||
|
self.db,
|
||||||
|
query,
|
||||||
|
is_page=True,
|
||||||
|
warehouse_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(marker, actual)
|
||||||
|
statement = paginate.await_args.args[1]
|
||||||
|
mysql_sql = compile_sql(statement, mysql.dialect())
|
||||||
|
postgresql_sql = compile_sql(statement, postgresql.dialect())
|
||||||
|
self.assertIn('WHERE false = 1', mysql_sql)
|
||||||
|
self.assertIn('WHERE false', postgresql_sql)
|
||||||
|
self.assertEqual((query.page_num, query.page_size, True), paginate.await_args.args[2:])
|
||||||
|
|
||||||
|
async def test_inbound_list_none_scope_has_no_authorization_predicate(self) -> None:
|
||||||
|
query = InboundPageQueryModel()
|
||||||
|
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=object())) as paginate:
|
||||||
|
await OperationDao.get_inbound_list(
|
||||||
|
self.db,
|
||||||
|
query,
|
||||||
|
is_page=True,
|
||||||
|
warehouse_ids=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
sql = compile_sql(paginate.await_args.args[1])
|
||||||
|
self.assertNotIn('warehouse_id IN', sql)
|
||||||
|
self.assertNotIn('WHERE false', sql)
|
||||||
|
|
||||||
def test_time_conditions_include_fractional_last_second_with_exclusive_next_day(self) -> None:
|
def test_time_conditions_include_fractional_last_second_with_exclusive_next_day(self) -> None:
|
||||||
lower, upper = OperationDao._time_conditions(
|
lower, upper = OperationDao._time_conditions(
|
||||||
BizUsageRecord.occurred_at,
|
BizUsageRecord.occurred_at,
|
||||||
|
|||||||
Reference in New Issue
Block a user