安全:补齐业务批次归属校验
This commit is contained in:
@@ -16,7 +16,7 @@ from module_business.dao.master_dao import MasterDao
|
|||||||
from module_business.dao.operation_dao import OperationDao
|
from module_business.dao.operation_dao import OperationDao
|
||||||
from module_business.domain.enums import PalletEventType, PalletFlowState
|
from module_business.domain.enums import PalletEventType, PalletFlowState
|
||||||
from module_business.domain.pallet_state_machine import PalletStateMachine
|
from module_business.domain.pallet_state_machine import PalletStateMachine
|
||||||
from module_business.domain.test_batch_scope import TestBatchScope
|
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
|
||||||
from module_business.entity.do.master_do import BizPallet, BizStrain
|
from module_business.entity.do.master_do import BizPallet, BizStrain
|
||||||
from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory
|
from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory
|
||||||
from module_business.entity.vo.master_vo import StrainPageQueryModel
|
from module_business.entity.vo.master_vo import StrainPageQueryModel
|
||||||
@@ -260,9 +260,11 @@ class FillingService:
|
|||||||
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
||||||
if pallet is None:
|
if pallet is None:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
||||||
if inventory is None:
|
if inventory is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
if inventory.warehouse_id is None:
|
if inventory.warehouse_id is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘不在仓库')
|
cls._raise_pallet_not_accessible(current_user, '托盘不在仓库')
|
||||||
await cls._assert_pallet_warehouse_access(
|
await cls._assert_pallet_warehouse_access(
|
||||||
@@ -351,6 +353,7 @@ class FillingService:
|
|||||||
warehouse_ids=[page_object.warehouse_id],
|
warehouse_ids=[page_object.warehouse_id],
|
||||||
)
|
)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
|
assert_record_in_scope(existing, test_scope, '装填记录')
|
||||||
await AccessService.assert_warehouse_access(
|
await AccessService.assert_warehouse_access(
|
||||||
query_db,
|
query_db,
|
||||||
current_user,
|
current_user,
|
||||||
@@ -375,6 +378,7 @@ class FillingService:
|
|||||||
)
|
)
|
||||||
if scanned_pallet is None:
|
if scanned_pallet is None:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(scanned_pallet, test_scope, '托盘')
|
||||||
|
|
||||||
inventory = await InventoryDao.get_inventory_for_update(
|
inventory = await InventoryDao.get_inventory_for_update(
|
||||||
query_db,
|
query_db,
|
||||||
@@ -383,6 +387,7 @@ class FillingService:
|
|||||||
)
|
)
|
||||||
if inventory is None:
|
if inventory is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
if inventory.warehouse_id is None:
|
if inventory.warehouse_id is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘不在仓库')
|
cls._raise_pallet_not_accessible(current_user, '托盘不在仓库')
|
||||||
await cls._assert_pallet_warehouse_access(
|
await cls._assert_pallet_warehouse_access(
|
||||||
@@ -398,6 +403,7 @@ class FillingService:
|
|||||||
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != scanned_pallet.pallet_id:
|
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != scanned_pallet.pallet_id:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
pallet = locked_pallets[0]
|
pallet = locked_pallets[0]
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
|
|
||||||
if inventory.active_filling_id is not None:
|
if inventory.active_filling_id is not None:
|
||||||
concurrent = await OperationDao.get_active_filling_for_update(
|
concurrent = await OperationDao.get_active_filling_for_update(
|
||||||
@@ -406,6 +412,7 @@ class FillingService:
|
|||||||
test_scope,
|
test_scope,
|
||||||
)
|
)
|
||||||
if concurrent is not None and concurrent.idempotency_key == page_object.idempotency_key:
|
if concurrent is not None and concurrent.idempotency_key == page_object.idempotency_key:
|
||||||
|
assert_record_in_scope(concurrent, test_scope, '活动装填')
|
||||||
# 结束首次非锁定读取建立的 RR 快照,再按最新数据范围恢复并发成功结果。
|
# 结束首次非锁定读取建立的 RR 快照,再按最新数据范围恢复并发成功结果。
|
||||||
await query_db.rollback()
|
await query_db.rollback()
|
||||||
recovery_transaction_ended = True
|
recovery_transaction_ended = True
|
||||||
@@ -425,6 +432,7 @@ class FillingService:
|
|||||||
)
|
)
|
||||||
if warehouse is None:
|
if warehouse is None:
|
||||||
raise ServiceException(message='仓库不存在')
|
raise ServiceException(message='仓库不存在')
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
if warehouse.status != '0':
|
if warehouse.status != '0':
|
||||||
raise ServiceException(message='仓库已停用')
|
raise ServiceException(message='仓库已停用')
|
||||||
await cls._assert_pallet_warehouse_access(
|
await cls._assert_pallet_warehouse_access(
|
||||||
@@ -441,6 +449,7 @@ class FillingService:
|
|||||||
for_update=True,
|
for_update=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
assert_record_in_scope(strain, test_scope, '菌种')
|
||||||
|
|
||||||
cls._validate_empty_warehouse_pallet(
|
cls._validate_empty_warehouse_pallet(
|
||||||
pallet,
|
pallet,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from module_business.dao.master_dao import MasterDao
|
|||||||
from module_business.dao.operation_dao import OperationDao
|
from module_business.dao.operation_dao import OperationDao
|
||||||
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
|
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
|
||||||
from module_business.domain.pallet_state_machine import PalletStateMachine
|
from module_business.domain.pallet_state_machine import PalletStateMachine
|
||||||
from module_business.domain.test_batch_scope import TestBatchScope
|
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
|
||||||
from module_business.entity.do.master_do import BizPallet
|
from module_business.entity.do.master_do import BizPallet
|
||||||
from module_business.entity.do.operation_do import (
|
from module_business.entity.do.operation_do import (
|
||||||
BizInboundItem,
|
BizInboundItem,
|
||||||
@@ -103,6 +103,7 @@ class InboundService:
|
|||||||
warehouse = await MasterDao.get_warehouse(query_db, warehouse_id, test_scope)
|
warehouse = await MasterDao.get_warehouse(query_db, warehouse_id, test_scope)
|
||||||
if warehouse is None:
|
if warehouse is None:
|
||||||
raise ServiceException(message='目标仓库不存在')
|
raise ServiceException(message='目标仓库不存在')
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
if warehouse.status != '0':
|
if warehouse.status != '0':
|
||||||
raise ServiceException(message='目标仓库已停用')
|
raise ServiceException(message='目标仓库已停用')
|
||||||
return warehouse
|
return warehouse
|
||||||
@@ -128,6 +129,8 @@ class InboundService:
|
|||||||
test_scope,
|
test_scope,
|
||||||
for_update=True,
|
for_update=True,
|
||||||
)
|
)
|
||||||
|
if warehouses[warehouse_id] is not None:
|
||||||
|
assert_record_in_scope(warehouses[warehouse_id], test_scope, '仓库')
|
||||||
try:
|
try:
|
||||||
await AccessService.assert_warehouse_access(
|
await AccessService.assert_warehouse_access(
|
||||||
query_db,
|
query_db,
|
||||||
@@ -195,6 +198,10 @@ class InboundService:
|
|||||||
ordered_ids = sorted(pallet_ids)
|
ordered_ids = sorted(pallet_ids)
|
||||||
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
|
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
|
||||||
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
|
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
|
||||||
|
for inventory in inventories:
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
|
for pallet in pallets:
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
expected_ids = set(ordered_ids)
|
expected_ids = set(ordered_ids)
|
||||||
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
|
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
|
||||||
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
|
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
|
||||||
@@ -327,15 +334,18 @@ class InboundService:
|
|||||||
warehouse = await MasterDao.get_warehouse(query_db, query_object.warehouse_id, test_scope)
|
warehouse = await MasterDao.get_warehouse(query_db, query_object.warehouse_id, test_scope)
|
||||||
if warehouse is None:
|
if warehouse is None:
|
||||||
raise ServiceException(message='目标仓库不存在')
|
raise ServiceException(message='目标仓库不存在')
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
if warehouse.status != '0':
|
if warehouse.status != '0':
|
||||||
raise ServiceException(message='目标仓库已停用')
|
raise ServiceException(message='目标仓库已停用')
|
||||||
|
|
||||||
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
||||||
if pallet is None:
|
if pallet is None:
|
||||||
raise ServiceException(message='托盘不存在')
|
raise ServiceException(message='托盘不存在')
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
||||||
if inventory is None:
|
if inventory is None:
|
||||||
raise ServiceException(message='托盘库存不存在')
|
raise ServiceException(message='托盘库存不存在')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
cls._validate_pallet_pair(pallet, inventory)
|
cls._validate_pallet_pair(pallet, inventory)
|
||||||
return PalletScanResponseModel(
|
return PalletScanResponseModel(
|
||||||
pallet_id=pallet.pallet_id,
|
pallet_id=pallet.pallet_id,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from module_business.dao.master_dao import MasterDao
|
|||||||
from module_business.dao.operation_dao import OperationDao
|
from module_business.dao.operation_dao import OperationDao
|
||||||
from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus
|
from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus
|
||||||
from module_business.domain.pallet_state_machine import PalletStateMachine
|
from module_business.domain.pallet_state_machine import PalletStateMachine
|
||||||
from module_business.domain.test_batch_scope import TestBatchScope
|
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
|
||||||
from module_business.entity.do.master_do import BizPallet
|
from module_business.entity.do.master_do import BizPallet
|
||||||
from module_business.entity.do.operation_do import (
|
from module_business.entity.do.operation_do import (
|
||||||
BizFillingRecord,
|
BizFillingRecord,
|
||||||
@@ -256,6 +256,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if return_order is None:
|
if return_order is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(return_order, test_scope, '退回单')
|
||||||
return ReturnModel.model_validate(return_order)
|
return ReturnModel.model_validate(return_order)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -346,9 +347,11 @@ class ReturnService:
|
|||||||
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
||||||
if pallet is None:
|
if pallet is None:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
||||||
if inventory is None or inventory.site_id is None:
|
if inventory is None or inventory.site_id is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在或不在客户现场')
|
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在或不在客户现场')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id)
|
await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id)
|
||||||
cls._validate_request_inventory(pallet, inventory, inventory.site_id)
|
cls._validate_request_inventory(pallet, inventory, inventory.site_id)
|
||||||
|
|
||||||
@@ -357,6 +360,7 @@ class ReturnService:
|
|||||||
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
||||||
if filling is None:
|
if filling is None:
|
||||||
raise ServiceException(message='托盘活动装填周期不存在')
|
raise ServiceException(message='托盘活动装填周期不存在')
|
||||||
|
assert_record_in_scope(filling, test_scope, '活动装填')
|
||||||
cls._validate_active_filling(filling, inventory)
|
cls._validate_active_filling(filling, inventory)
|
||||||
return cls._scan_response(pallet, inventory, filling, ['request'])
|
return cls._scan_response(pallet, inventory, filling, ['request'])
|
||||||
|
|
||||||
@@ -380,18 +384,22 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if return_order is None:
|
if return_order is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(return_order, test_scope, '退回单')
|
||||||
if return_order.status != ReturnStatus.REQUESTED.value:
|
if return_order.status != ReturnStatus.REQUESTED.value:
|
||||||
raise ServiceException(message='退回单当前状态不允许确认扫码')
|
raise ServiceException(message='退回单当前状态不允许确认扫码')
|
||||||
|
|
||||||
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
||||||
if pallet is None:
|
if pallet is None:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
matching_items = [item for item in return_order.items if item.pallet_id == pallet.pallet_id]
|
matching_items = [item for item in return_order.items if item.pallet_id == pallet.pallet_id]
|
||||||
if len(matching_items) != 1:
|
if len(matching_items) != 1:
|
||||||
cls._raise_pallet_not_accessible(current_user, '扫码托盘不属于该退回单')
|
cls._raise_pallet_not_accessible(current_user, '扫码托盘不属于该退回单')
|
||||||
|
assert_record_in_scope(matching_items[0], test_scope, '退回单明细')
|
||||||
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
||||||
if inventory is None:
|
if inventory is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
item = matching_items[0]
|
item = matching_items[0]
|
||||||
cls._validate_confirm_snapshot(return_order, item, pallet, inventory)
|
cls._validate_confirm_snapshot(return_order, item, pallet, inventory)
|
||||||
|
|
||||||
@@ -400,6 +408,7 @@ class ReturnService:
|
|||||||
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
||||||
if filling is None:
|
if filling is None:
|
||||||
raise ServiceException(message='托盘活动装填周期不存在')
|
raise ServiceException(message='托盘活动装填周期不存在')
|
||||||
|
assert_record_in_scope(filling, test_scope, '活动装填')
|
||||||
cls._validate_active_filling(filling, inventory)
|
cls._validate_active_filling(filling, inventory)
|
||||||
return cls._scan_response(pallet, inventory, filling, ['confirm'])
|
return cls._scan_response(pallet, inventory, filling, ['confirm'])
|
||||||
|
|
||||||
@@ -472,6 +481,8 @@ class ReturnService:
|
|||||||
cls._raise_pallet_not_accessible(current_user, '存在不存在或库存缺失的托盘')
|
cls._raise_pallet_not_accessible(current_user, '存在不存在或库存缺失的托盘')
|
||||||
|
|
||||||
ordered_inventories = [inventory_by_id[pallet_id] for pallet_id in ordered_ids]
|
ordered_inventories = [inventory_by_id[pallet_id] for pallet_id in ordered_ids]
|
||||||
|
for inventory in ordered_inventories:
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
if any(inventory.site_id != source_site_id for inventory in ordered_inventories):
|
if any(inventory.site_id != source_site_id for inventory in ordered_inventories):
|
||||||
cls._raise_pallet_not_accessible(current_user, '同一退回单托盘必须属于同一现场')
|
cls._raise_pallet_not_accessible(current_user, '同一退回单托盘必须属于同一现场')
|
||||||
if any(
|
if any(
|
||||||
@@ -485,6 +496,8 @@ class ReturnService:
|
|||||||
if set(pallet_by_id) != set(ordered_ids):
|
if set(pallet_by_id) != set(ordered_ids):
|
||||||
cls._raise_pallet_not_accessible(current_user, '存在不存在或库存缺失的托盘')
|
cls._raise_pallet_not_accessible(current_user, '存在不存在或库存缺失的托盘')
|
||||||
ordered_pallets = [pallet_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 in ordered_pallets:
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
|
|
||||||
filling_by_id: dict[int, BizFillingRecord | None] = {}
|
filling_by_id: dict[int, BizFillingRecord | None] = {}
|
||||||
filling_ids = sorted(
|
filling_ids = sorted(
|
||||||
@@ -560,6 +573,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if warehouse is None:
|
if warehouse is None:
|
||||||
raise ServiceException(message='目标仓库不存在')
|
raise ServiceException(message='目标仓库不存在')
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
if warehouse.status != '0':
|
if warehouse.status != '0':
|
||||||
raise ServiceException(message='目标仓库已停用')
|
raise ServiceException(message='目标仓库已停用')
|
||||||
|
|
||||||
@@ -572,6 +586,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if site is None:
|
if site is None:
|
||||||
raise ServiceException(message='来源客户现场不存在')
|
raise ServiceException(message='来源客户现场不存在')
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
if site.status != '0':
|
if site.status != '0':
|
||||||
raise ServiceException(message='来源客户现场已停用')
|
raise ServiceException(message='来源客户现场已停用')
|
||||||
|
|
||||||
@@ -595,6 +610,7 @@ class ReturnService:
|
|||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
if site is None:
|
if site is None:
|
||||||
raise ServiceException(message='来源客户现场不存在')
|
raise ServiceException(message='来源客户现场不存在')
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _recover_request_integrity_error(
|
async def _recover_request_integrity_error(
|
||||||
@@ -620,6 +636,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if recovered is None or not cls._is_exact_request_result(recovered, page_object, request_hash):
|
if recovered is None or not cls._is_exact_request_result(recovered, page_object, request_hash):
|
||||||
raise ServiceException(message='申请退回失败,幂等冲突')
|
raise ServiceException(message='申请退回失败,幂等冲突')
|
||||||
|
assert_record_in_scope(recovered, test_scope, '退回单')
|
||||||
locked = await OperationDao.get_return_for_update(
|
locked = await OperationDao.get_return_for_update(
|
||||||
query_db,
|
query_db,
|
||||||
recovered.return_id,
|
recovered.return_id,
|
||||||
@@ -629,6 +646,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if locked is None:
|
if locked is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(locked, test_scope, '退回单')
|
||||||
if not cls._is_exact_request_result(
|
if not cls._is_exact_request_result(
|
||||||
locked,
|
locked,
|
||||||
page_object,
|
page_object,
|
||||||
@@ -689,6 +707,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if locked_existing is None:
|
if locked_existing is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(locked_existing, test_scope, '退回单')
|
||||||
if not cls._is_exact_request_result(
|
if not cls._is_exact_request_result(
|
||||||
locked_existing,
|
locked_existing,
|
||||||
page_object,
|
page_object,
|
||||||
@@ -721,6 +740,7 @@ class ReturnService:
|
|||||||
scope=test_scope,
|
scope=test_scope,
|
||||||
)
|
)
|
||||||
if fresh_existing is not None:
|
if fresh_existing is not None:
|
||||||
|
assert_record_in_scope(fresh_existing, test_scope, '退回单')
|
||||||
if not cls._is_exact_request_result(fresh_existing, page_object, request_hash):
|
if not cls._is_exact_request_result(fresh_existing, page_object, request_hash):
|
||||||
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
|
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
|
||||||
fresh_existing_id = int(fresh_existing.return_id)
|
fresh_existing_id = int(fresh_existing.return_id)
|
||||||
@@ -863,6 +883,9 @@ class ReturnService:
|
|||||||
test_scope: TestBatchScope,
|
test_scope: TestBatchScope,
|
||||||
) -> tuple[list[BizPalletInventory], dict[int, BizFillingRecord]]:
|
) -> tuple[list[BizPalletInventory], dict[int, BizFillingRecord]]:
|
||||||
ordered_ids = sorted(item.pallet_id for item in items)
|
ordered_ids = sorted(item.pallet_id for item in items)
|
||||||
|
assert_record_in_scope(return_order, test_scope, '退回单')
|
||||||
|
for item in items:
|
||||||
|
assert_record_in_scope(item, test_scope, '退回单明细')
|
||||||
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
|
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
|
||||||
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
|
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
|
||||||
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
|
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
|
||||||
@@ -876,6 +899,10 @@ class ReturnService:
|
|||||||
raise ServiceException(message='退回单存在不存在或库存缺失的托盘')
|
raise ServiceException(message='退回单存在不存在或库存缺失的托盘')
|
||||||
|
|
||||||
ordered_inventories = [inventory_by_id[pallet_id] for pallet_id in ordered_ids]
|
ordered_inventories = [inventory_by_id[pallet_id] for pallet_id in ordered_ids]
|
||||||
|
for inventory in ordered_inventories:
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
|
for pallet in pallet_by_id.values():
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
for pallet_id in ordered_ids:
|
for pallet_id in ordered_ids:
|
||||||
cls._validate_confirm_snapshot(
|
cls._validate_confirm_snapshot(
|
||||||
return_order,
|
return_order,
|
||||||
@@ -896,6 +923,7 @@ class ReturnService:
|
|||||||
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
|
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
|
||||||
if filling is None:
|
if filling is None:
|
||||||
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
|
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
|
||||||
|
assert_record_in_scope(filling, test_scope, '活动装填')
|
||||||
filling_by_id[filling_id] = filling
|
filling_by_id[filling_id] = filling
|
||||||
for inventory in ordered_inventories:
|
for inventory in ordered_inventories:
|
||||||
if inventory.active_filling_id is not None:
|
if inventory.active_filling_id is not None:
|
||||||
@@ -929,6 +957,7 @@ class ReturnService:
|
|||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
if warehouse is None:
|
if warehouse is None:
|
||||||
raise ServiceException(message='目标仓库不存在')
|
raise ServiceException(message='目标仓库不存在')
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
if require_active and warehouse.status != '0':
|
if require_active and warehouse.status != '0':
|
||||||
raise ServiceException(message='目标仓库已停用')
|
raise ServiceException(message='目标仓库已停用')
|
||||||
|
|
||||||
@@ -955,6 +984,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if route is None:
|
if route is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(route, test_scope, '退回单')
|
||||||
recovered = await OperationDao.get_return_by_confirm_idempotency_key(
|
recovered = await OperationDao.get_return_by_confirm_idempotency_key(
|
||||||
query_db,
|
query_db,
|
||||||
page_object.idempotency_key,
|
page_object.idempotency_key,
|
||||||
@@ -964,6 +994,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if recovered is None:
|
if recovered is None:
|
||||||
raise ServiceException(message='确认退回失败,幂等冲突')
|
raise ServiceException(message='确认退回失败,幂等冲突')
|
||||||
|
assert_record_in_scope(recovered, test_scope, '退回单')
|
||||||
if not cls._is_exact_confirm_result(recovered, return_id, page_object, request_hash):
|
if not cls._is_exact_confirm_result(recovered, return_id, page_object, request_hash):
|
||||||
raise ServiceException(message='确认退回失败,幂等键已被其他请求使用')
|
raise ServiceException(message='确认退回失败,幂等键已被其他请求使用')
|
||||||
locked = await OperationDao.get_return_for_update(
|
locked = await OperationDao.get_return_for_update(
|
||||||
@@ -975,6 +1006,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if locked is None:
|
if locked is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(locked, test_scope, '退回单')
|
||||||
if not cls._is_exact_confirm_result(
|
if not cls._is_exact_confirm_result(
|
||||||
locked,
|
locked,
|
||||||
return_id,
|
return_id,
|
||||||
@@ -1019,6 +1051,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if return_order is None:
|
if return_order is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(return_order, test_scope, '退回单')
|
||||||
try:
|
try:
|
||||||
await AccessService.assert_warehouse_access(
|
await AccessService.assert_warehouse_access(
|
||||||
query_db,
|
query_db,
|
||||||
@@ -1041,6 +1074,8 @@ class ReturnService:
|
|||||||
request_hash,
|
request_hash,
|
||||||
):
|
):
|
||||||
raise ServiceException(message='确认退回失败,幂等键已被其他请求使用')
|
raise ServiceException(message='确认退回失败,幂等键已被其他请求使用')
|
||||||
|
if existing is not None:
|
||||||
|
assert_record_in_scope(existing, test_scope, '退回单')
|
||||||
|
|
||||||
await query_db.rollback()
|
await query_db.rollback()
|
||||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||||
@@ -1053,6 +1088,7 @@ class ReturnService:
|
|||||||
)
|
)
|
||||||
if locked is None:
|
if locked is None:
|
||||||
cls._raise_return_not_accessible(current_user)
|
cls._raise_return_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(locked, test_scope, '退回单')
|
||||||
if locked.status == ReturnStatus.CONFIRMED.value:
|
if locked.status == ReturnStatus.CONFIRMED.value:
|
||||||
if cls._is_exact_confirm_result(
|
if cls._is_exact_confirm_result(
|
||||||
locked,
|
locked,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from module_business.dao.master_dao import MasterDao
|
|||||||
from module_business.dao.operation_dao import OperationDao
|
from module_business.dao.operation_dao import OperationDao
|
||||||
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
|
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
|
||||||
from module_business.domain.pallet_state_machine import PalletStateMachine
|
from module_business.domain.pallet_state_machine import PalletStateMachine
|
||||||
from module_business.domain.test_batch_scope import TestBatchScope
|
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
|
||||||
from module_business.entity.do.master_do import BizPallet
|
from module_business.entity.do.master_do import BizPallet
|
||||||
from module_business.entity.do.operation_do import (
|
from module_business.entity.do.operation_do import (
|
||||||
BizFillingRecord,
|
BizFillingRecord,
|
||||||
@@ -105,12 +105,14 @@ class ShipmentService:
|
|||||||
warehouse = await MasterDao.get_warehouse(query_db, source_warehouse_id, test_scope)
|
warehouse = await MasterDao.get_warehouse(query_db, source_warehouse_id, test_scope)
|
||||||
if warehouse is None:
|
if warehouse is None:
|
||||||
raise ServiceException(message='来源仓库不存在')
|
raise ServiceException(message='来源仓库不存在')
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
if warehouse.status != '0':
|
if warehouse.status != '0':
|
||||||
raise ServiceException(message='来源仓库已停用')
|
raise ServiceException(message='来源仓库已停用')
|
||||||
await AccessService.assert_site_access(query_db, current_user, target_site_id)
|
await AccessService.assert_site_access(query_db, current_user, target_site_id)
|
||||||
site = await MasterDao.get_site(query_db, target_site_id, test_scope)
|
site = await MasterDao.get_site(query_db, target_site_id, test_scope)
|
||||||
if site is None:
|
if site is None:
|
||||||
raise ServiceException(message='目标现场不存在')
|
raise ServiceException(message='目标现场不存在')
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
if site.status != '0':
|
if site.status != '0':
|
||||||
raise ServiceException(message='目标现场已停用')
|
raise ServiceException(message='目标现场已停用')
|
||||||
return warehouse, site
|
return warehouse, site
|
||||||
@@ -166,6 +168,10 @@ class ShipmentService:
|
|||||||
ordered_ids = cls._validate_pallet_ids(pallet_ids)
|
ordered_ids = cls._validate_pallet_ids(pallet_ids)
|
||||||
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
|
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
|
||||||
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
|
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
|
||||||
|
for inventory in inventories:
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
|
for pallet in pallets:
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
if (
|
if (
|
||||||
len(inventories) != len(ordered_ids)
|
len(inventories) != len(ordered_ids)
|
||||||
or len(pallets) != len(ordered_ids)
|
or len(pallets) != len(ordered_ids)
|
||||||
@@ -188,6 +194,7 @@ class ShipmentService:
|
|||||||
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
|
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
|
||||||
if filling is None:
|
if filling is None:
|
||||||
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
|
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
|
||||||
|
assert_record_in_scope(filling, test_scope, '活动装填')
|
||||||
fillings.append(filling)
|
fillings.append(filling)
|
||||||
if len(fillings) != len(ordered_ids):
|
if len(fillings) != len(ordered_ids):
|
||||||
raise ServiceException(message='托盘活动装填周期不完整')
|
raise ServiceException(message='托盘活动装填周期不完整')
|
||||||
@@ -412,6 +419,7 @@ class ShipmentService:
|
|||||||
warehouse = await MasterDao.get_warehouse(query_db, query_object.source_warehouse_id, test_scope)
|
warehouse = await MasterDao.get_warehouse(query_db, query_object.source_warehouse_id, test_scope)
|
||||||
if warehouse is None:
|
if warehouse is None:
|
||||||
raise ServiceException(message='来源仓库不存在')
|
raise ServiceException(message='来源仓库不存在')
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
if warehouse.status != '0':
|
if warehouse.status != '0':
|
||||||
raise ServiceException(message='来源仓库已停用')
|
raise ServiceException(message='来源仓库已停用')
|
||||||
await AccessService.assert_site_access(
|
await AccessService.assert_site_access(
|
||||||
@@ -422,15 +430,18 @@ class ShipmentService:
|
|||||||
site = await MasterDao.get_site(query_db, query_object.target_site_id, test_scope)
|
site = await MasterDao.get_site(query_db, query_object.target_site_id, test_scope)
|
||||||
if site is None:
|
if site is None:
|
||||||
raise ServiceException(message='目标现场不存在')
|
raise ServiceException(message='目标现场不存在')
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
if site.status != '0':
|
if site.status != '0':
|
||||||
raise ServiceException(message='目标现场已停用')
|
raise ServiceException(message='目标现场已停用')
|
||||||
|
|
||||||
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
|
||||||
if pallet is None:
|
if pallet is None:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
||||||
if inventory is None:
|
if inventory is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
if inventory.pallet_id != pallet.pallet_id or inventory.warehouse_id != query_object.source_warehouse_id:
|
if inventory.pallet_id != pallet.pallet_id or inventory.warehouse_id != query_object.source_warehouse_id:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘不在来源仓库')
|
cls._raise_pallet_not_accessible(current_user, '托盘不在来源仓库')
|
||||||
if inventory.site_id is not None or inventory.current_line_id is not None:
|
if inventory.site_id is not None or inventory.current_line_id is not None:
|
||||||
@@ -440,6 +451,7 @@ class ShipmentService:
|
|||||||
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
||||||
if filling is None:
|
if filling is None:
|
||||||
raise ServiceException(message='托盘活动装填周期不存在')
|
raise ServiceException(message='托盘活动装填周期不存在')
|
||||||
|
assert_record_in_scope(filling, test_scope, '活动装填')
|
||||||
cls._validate_scan_pallet(
|
cls._validate_scan_pallet(
|
||||||
pallet,
|
pallet,
|
||||||
inventory,
|
inventory,
|
||||||
@@ -629,7 +641,7 @@ class ShipmentService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _lock_and_authorize_routes(
|
async def _lock_and_authorize_routes( # noqa: PLR0912
|
||||||
cls,
|
cls,
|
||||||
query_db: AsyncSession,
|
query_db: AsyncSession,
|
||||||
current_user: CurrentUserModel,
|
current_user: CurrentUserModel,
|
||||||
@@ -651,6 +663,8 @@ class ShipmentService:
|
|||||||
for_update=True,
|
for_update=True,
|
||||||
)
|
)
|
||||||
warehouses[warehouse_id] = warehouse
|
warehouses[warehouse_id] = warehouse
|
||||||
|
if warehouse is not None:
|
||||||
|
assert_record_in_scope(warehouse, test_scope, '仓库')
|
||||||
try:
|
try:
|
||||||
await AccessService.assert_warehouse_access(
|
await AccessService.assert_warehouse_access(
|
||||||
query_db,
|
query_db,
|
||||||
@@ -671,6 +685,8 @@ class ShipmentService:
|
|||||||
for_update=True,
|
for_update=True,
|
||||||
)
|
)
|
||||||
sites[site_id] = site
|
sites[site_id] = site
|
||||||
|
if site is not None:
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
try:
|
try:
|
||||||
await AccessService.assert_site_access(
|
await AccessService.assert_site_access(
|
||||||
query_db,
|
query_db,
|
||||||
@@ -749,6 +765,7 @@ class ShipmentService:
|
|||||||
)
|
)
|
||||||
if current is None:
|
if current is None:
|
||||||
cls._raise_shipment_not_accessible(current_user)
|
cls._raise_shipment_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(current, test_scope, '发货单')
|
||||||
recovered = await OperationDao.get_shipment_by_confirm_idempotency_key(
|
recovered = await OperationDao.get_shipment_by_confirm_idempotency_key(
|
||||||
query_db,
|
query_db,
|
||||||
idempotency_key,
|
idempotency_key,
|
||||||
@@ -758,6 +775,7 @@ class ShipmentService:
|
|||||||
)
|
)
|
||||||
if recovered is None:
|
if recovered is None:
|
||||||
raise ServiceException(message='确认发货失败,幂等冲突')
|
raise ServiceException(message='确认发货失败,幂等冲突')
|
||||||
|
assert_record_in_scope(recovered, test_scope, '发货单')
|
||||||
if not cls._is_exact_confirmed(
|
if not cls._is_exact_confirmed(
|
||||||
recovered,
|
recovered,
|
||||||
shipment_id,
|
shipment_id,
|
||||||
@@ -774,6 +792,7 @@ class ShipmentService:
|
|||||||
)
|
)
|
||||||
if locked_recovered is None:
|
if locked_recovered is None:
|
||||||
cls._raise_shipment_not_accessible(current_user)
|
cls._raise_shipment_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(locked_recovered, test_scope, '发货单')
|
||||||
if not cls._is_exact_confirmed(
|
if not cls._is_exact_confirmed(
|
||||||
locked_recovered,
|
locked_recovered,
|
||||||
shipment_id,
|
shipment_id,
|
||||||
@@ -814,6 +833,7 @@ class ShipmentService:
|
|||||||
)
|
)
|
||||||
if shipment is None:
|
if shipment is None:
|
||||||
cls._raise_shipment_not_accessible(current_user)
|
cls._raise_shipment_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(shipment, test_scope, '发货单')
|
||||||
|
|
||||||
request_hash = cls._build_confirm_request_hash(shipment_id)
|
request_hash = cls._build_confirm_request_hash(shipment_id)
|
||||||
idempotency_key = page_object.idempotency_key
|
idempotency_key = page_object.idempotency_key
|
||||||
@@ -841,6 +861,7 @@ class ShipmentService:
|
|||||||
)
|
)
|
||||||
if locked_shipment is None:
|
if locked_shipment is None:
|
||||||
cls._raise_shipment_not_accessible(current_user)
|
cls._raise_shipment_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(locked_shipment, test_scope, '发货单')
|
||||||
if locked_shipment.status == DocumentStatus.CONFIRMED.value:
|
if locked_shipment.status == DocumentStatus.CONFIRMED.value:
|
||||||
if cls._is_exact_confirmed(
|
if cls._is_exact_confirmed(
|
||||||
locked_shipment,
|
locked_shipment,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from module_business.dao.master_dao import MasterDao
|
|||||||
from module_business.dao.operation_dao import OperationDao
|
from module_business.dao.operation_dao import OperationDao
|
||||||
from module_business.domain.enums import PalletEventType, PalletFlowState
|
from module_business.domain.enums import PalletEventType, PalletFlowState
|
||||||
from module_business.domain.pallet_state_machine import PalletStateMachine
|
from module_business.domain.pallet_state_machine import PalletStateMachine
|
||||||
from module_business.domain.test_batch_scope import TestBatchScope
|
from module_business.domain.test_batch_scope import TestBatchScope, assert_record_in_scope
|
||||||
from module_business.entity.do.operation_do import BizPalletEvent, BizUsageRecord
|
from module_business.entity.do.operation_do import BizPalletEvent, BizUsageRecord
|
||||||
from module_business.entity.vo.master_vo import LinePageQueryModel
|
from module_business.entity.vo.master_vo import LinePageQueryModel
|
||||||
from module_business.entity.vo.operation_vo import (
|
from module_business.entity.vo.operation_vo import (
|
||||||
@@ -165,6 +165,7 @@ class UsageService:
|
|||||||
cls._raise_usage_not_accessible(current_user)
|
cls._raise_usage_not_accessible(current_user)
|
||||||
if site is None:
|
if site is None:
|
||||||
raise ServiceException(message='客户现场不存在')
|
raise ServiceException(message='客户现场不存在')
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _recover_integrity_error(
|
async def _recover_integrity_error(
|
||||||
@@ -189,6 +190,7 @@ class UsageService:
|
|||||||
)
|
)
|
||||||
if recovered is None:
|
if recovered is None:
|
||||||
raise ServiceException(message='使用失败,数据冲突')
|
raise ServiceException(message='使用失败,数据冲突')
|
||||||
|
assert_record_in_scope(recovered, test_scope, '使用记录')
|
||||||
if recovered.site_id != expected_site_id or not cls._is_exact_idempotent_result(
|
if recovered.site_id != expected_site_id or not cls._is_exact_idempotent_result(
|
||||||
recovered, page_object, request_hash
|
recovered, page_object, request_hash
|
||||||
):
|
):
|
||||||
@@ -255,6 +257,7 @@ class UsageService:
|
|||||||
)
|
)
|
||||||
if usage is None:
|
if usage is None:
|
||||||
cls._raise_usage_not_accessible(current_user)
|
cls._raise_usage_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(usage, test_scope, '使用记录')
|
||||||
return UsageModel.model_validate(usage)
|
return UsageModel.model_validate(usage)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -270,6 +273,7 @@ class UsageService:
|
|||||||
site = await MasterDao.get_site(query_db, site_id, test_scope)
|
site = await MasterDao.get_site(query_db, site_id, test_scope)
|
||||||
if site is None:
|
if site is None:
|
||||||
raise ServiceException(message='客户现场不存在')
|
raise ServiceException(message='客户现场不存在')
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
if site.status != '0':
|
if site.status != '0':
|
||||||
raise ServiceException(message='客户现场已停用')
|
raise ServiceException(message='客户现场已停用')
|
||||||
lines = await MasterDao.list_line(
|
lines = await MasterDao.list_line(
|
||||||
@@ -301,9 +305,11 @@ class UsageService:
|
|||||||
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, normalized_qr, test_scope)
|
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, normalized_qr, test_scope)
|
||||||
if pallet is None:
|
if pallet is None:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
|
||||||
if inventory is None:
|
if inventory is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
if inventory.site_id is None:
|
if inventory.site_id is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘不在客户现场')
|
cls._raise_pallet_not_accessible(current_user, '托盘不在客户现场')
|
||||||
await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id)
|
await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id)
|
||||||
@@ -328,6 +334,7 @@ class UsageService:
|
|||||||
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
|
||||||
if filling is None:
|
if filling is None:
|
||||||
raise ServiceException(message='托盘活动装填周期不存在')
|
raise ServiceException(message='托盘活动装填周期不存在')
|
||||||
|
assert_record_in_scope(filling, test_scope, '活动装填')
|
||||||
if (
|
if (
|
||||||
filling.filling_id != inventory.active_filling_id
|
filling.filling_id != inventory.active_filling_id
|
||||||
or filling.pallet_id != pallet.pallet_id
|
or filling.pallet_id != pallet.pallet_id
|
||||||
@@ -369,6 +376,8 @@ class UsageService:
|
|||||||
preflight_site_ids = await cls._site_scope(query_db, current_user)
|
preflight_site_ids = await cls._site_scope(query_db, current_user)
|
||||||
# 产线归属是请求内稳定路由;锁事务仍以库存中的实际现场复核。
|
# 产线归属是请求内稳定路由;锁事务仍以库存中的实际现场复核。
|
||||||
preflight_line = await MasterDao.get_line(query_db, page_object.line_id, test_scope)
|
preflight_line = await MasterDao.get_line(query_db, page_object.line_id, test_scope)
|
||||||
|
if preflight_line is not None:
|
||||||
|
assert_record_in_scope(preflight_line, test_scope, '产线')
|
||||||
preflight_site_id = preflight_line.site_id if preflight_line is not None else None
|
preflight_site_id = preflight_line.site_id if preflight_line is not None else None
|
||||||
existing = None
|
existing = None
|
||||||
if preflight_site_id is not None and (
|
if preflight_site_id is not None and (
|
||||||
@@ -381,6 +390,7 @@ class UsageService:
|
|||||||
site_ids=[preflight_site_id],
|
site_ids=[preflight_site_id],
|
||||||
)
|
)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
|
assert_record_in_scope(existing, test_scope, '使用记录')
|
||||||
if not cls._is_exact_idempotent_result(existing, page_object, request_hash):
|
if not cls._is_exact_idempotent_result(existing, page_object, request_hash):
|
||||||
raise ServiceException(message='使用失败,幂等键已被其他请求使用')
|
raise ServiceException(message='使用失败,幂等键已被其他请求使用')
|
||||||
if preflight_site_ids is not None and existing.site_id not in preflight_site_ids:
|
if preflight_site_ids is not None and existing.site_id not in preflight_site_ids:
|
||||||
@@ -402,6 +412,7 @@ class UsageService:
|
|||||||
)
|
)
|
||||||
if scanned_pallet is None:
|
if scanned_pallet is None:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
|
assert_record_in_scope(scanned_pallet, test_scope, '托盘')
|
||||||
scanned_pallet_id = int(scanned_pallet.pallet_id)
|
scanned_pallet_id = int(scanned_pallet.pallet_id)
|
||||||
await query_db.rollback()
|
await query_db.rollback()
|
||||||
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
|
||||||
@@ -413,6 +424,7 @@ class UsageService:
|
|||||||
)
|
)
|
||||||
if inventory is None:
|
if inventory is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
|
||||||
|
assert_record_in_scope(inventory, test_scope, '库存')
|
||||||
if inventory.site_id is None:
|
if inventory.site_id is None:
|
||||||
cls._raise_pallet_not_accessible(current_user, '托盘不在客户现场')
|
cls._raise_pallet_not_accessible(current_user, '托盘不在客户现场')
|
||||||
expected_site_id = inventory.site_id
|
expected_site_id = inventory.site_id
|
||||||
@@ -429,6 +441,7 @@ class UsageService:
|
|||||||
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != inventory.pallet_id:
|
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != inventory.pallet_id:
|
||||||
cls._raise_pallet_not_accessible(current_user)
|
cls._raise_pallet_not_accessible(current_user)
|
||||||
pallet = locked_pallets[0]
|
pallet = locked_pallets[0]
|
||||||
|
assert_record_in_scope(pallet, test_scope, '托盘')
|
||||||
|
|
||||||
filling = (
|
filling = (
|
||||||
await OperationDao.get_active_filling_for_update(
|
await OperationDao.get_active_filling_for_update(
|
||||||
@@ -446,6 +459,7 @@ class UsageService:
|
|||||||
site_ids=[inventory.site_id],
|
site_ids=[inventory.site_id],
|
||||||
)
|
)
|
||||||
if locked_existing is not None:
|
if locked_existing is not None:
|
||||||
|
assert_record_in_scope(locked_existing, test_scope, '使用记录')
|
||||||
if locked_existing.site_id != inventory.site_id or not cls._is_exact_idempotent_result(
|
if locked_existing.site_id != inventory.site_id or not cls._is_exact_idempotent_result(
|
||||||
locked_existing,
|
locked_existing,
|
||||||
page_object,
|
page_object,
|
||||||
@@ -466,6 +480,7 @@ class UsageService:
|
|||||||
raise ServiceException(message='托盘不存在活动装填周期')
|
raise ServiceException(message='托盘不存在活动装填周期')
|
||||||
if filling is None:
|
if filling is None:
|
||||||
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
|
raise ServiceException(message='托盘活动装填周期不存在或已关闭')
|
||||||
|
assert_record_in_scope(filling, test_scope, '活动装填')
|
||||||
|
|
||||||
site = await MasterDao.get_site(query_db, inventory.site_id, test_scope, for_update=True)
|
site = await MasterDao.get_site(query_db, inventory.site_id, test_scope, for_update=True)
|
||||||
await cls._assert_pallet_site_access(
|
await cls._assert_pallet_site_access(
|
||||||
@@ -476,11 +491,13 @@ class UsageService:
|
|||||||
)
|
)
|
||||||
if site is None:
|
if site is None:
|
||||||
raise ServiceException(message='客户现场不存在')
|
raise ServiceException(message='客户现场不存在')
|
||||||
|
assert_record_in_scope(site, test_scope, '现场')
|
||||||
if site.status != '0':
|
if site.status != '0':
|
||||||
raise ServiceException(message='客户现场已停用')
|
raise ServiceException(message='客户现场已停用')
|
||||||
line = await MasterDao.get_line(query_db, page_object.line_id, test_scope, for_update=True)
|
line = await MasterDao.get_line(query_db, page_object.line_id, test_scope, for_update=True)
|
||||||
if line is None:
|
if line is None:
|
||||||
raise ServiceException(message='产线不存在')
|
raise ServiceException(message='产线不存在')
|
||||||
|
assert_record_in_scope(line, test_scope, '产线')
|
||||||
if line.status != '0':
|
if line.status != '0':
|
||||||
raise ServiceException(message='产线已停用')
|
raise ServiceException(message='产线已停用')
|
||||||
if line.site_id != inventory.site_id:
|
if line.site_id != inventory.site_id:
|
||||||
@@ -540,6 +557,7 @@ class UsageService:
|
|||||||
test_batch_id=test_scope.batch_id,
|
test_batch_id=test_scope.batch_id,
|
||||||
)
|
)
|
||||||
usage = await OperationDao.add_usage_record(query_db, usage)
|
usage = await OperationDao.add_usage_record(query_db, usage)
|
||||||
|
assert_record_in_scope(usage, test_scope, '使用记录')
|
||||||
usage_id = usage.usage_id
|
usage_id = usage.usage_id
|
||||||
result = cls._success(usage)
|
result = cls._success(usage)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ from module_business.entity.vo.operation_vo import (
|
|||||||
)
|
)
|
||||||
from module_business.service.access_service import AccessService
|
from module_business.service.access_service import AccessService
|
||||||
from module_business.service.filling_service import FillingService
|
from module_business.service.filling_service import FillingService
|
||||||
|
from module_business.service.test_batch_access_service import TestBatchAccessService
|
||||||
|
from tests.module_business.test_batch_fixtures import make_test_scope
|
||||||
|
|
||||||
|
|
||||||
def integrity_error() -> IntegrityError:
|
def integrity_error() -> IntegrityError:
|
||||||
@@ -78,6 +80,7 @@ def make_pallet(
|
|||||||
qr_token: str = 'qr-2',
|
qr_token: str = 'qr-2',
|
||||||
capacity_packages: int = 20,
|
capacity_packages: int = 20,
|
||||||
asset_status: str = '0',
|
asset_status: str = '0',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizPallet:
|
) -> BizPallet:
|
||||||
return BizPallet(
|
return BizPallet(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
@@ -85,6 +88,7 @@ def make_pallet(
|
|||||||
qr_token=qr_token,
|
qr_token=qr_token,
|
||||||
capacity_packages=capacity_packages,
|
capacity_packages=capacity_packages,
|
||||||
asset_status=asset_status,
|
asset_status=asset_status,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +103,7 @@ def make_inventory(
|
|||||||
remaining_packages: int = 0,
|
remaining_packages: int = 0,
|
||||||
last_event_id: int | None = 40,
|
last_event_id: int | None = 40,
|
||||||
version: int = 3,
|
version: int = 3,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizPalletInventory:
|
) -> BizPalletInventory:
|
||||||
return BizPalletInventory(
|
return BizPalletInventory(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
@@ -111,6 +116,7 @@ def make_inventory(
|
|||||||
last_event_id=last_event_id,
|
last_event_id=last_event_id,
|
||||||
version=version,
|
version=version,
|
||||||
updated_at=datetime(2026, 7, 25, 9, 0),
|
updated_at=datetime(2026, 7, 25, 9, 0),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -120,12 +126,14 @@ def make_strain(
|
|||||||
status: str = '0',
|
status: str = '0',
|
||||||
code: str = 'S-011',
|
code: str = 'S-011',
|
||||||
name: str = '香菇 11',
|
name: str = '香菇 11',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizStrain:
|
) -> BizStrain:
|
||||||
return BizStrain(
|
return BizStrain(
|
||||||
strain_id=strain_id,
|
strain_id=strain_id,
|
||||||
strain_code=code,
|
strain_code=code,
|
||||||
strain_name=name,
|
strain_name=name,
|
||||||
status=status,
|
status=status,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -135,6 +143,7 @@ def make_filling(
|
|||||||
warehouse_id: int = 7,
|
warehouse_id: int = 7,
|
||||||
idempotency_key: str = 'fill-key',
|
idempotency_key: str = 'fill-key',
|
||||||
request_hash: str | None = None,
|
request_hash: str | None = None,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizFillingRecord:
|
) -> BizFillingRecord:
|
||||||
page = make_page(warehouse_id=warehouse_id, idempotency_key=idempotency_key)
|
page = make_page(warehouse_id=warehouse_id, idempotency_key=idempotency_key)
|
||||||
return BizFillingRecord(
|
return BizFillingRecord(
|
||||||
@@ -157,6 +166,7 @@ def make_filling(
|
|||||||
create_time=datetime(2026, 7, 26, 10, 30),
|
create_time=datetime(2026, 7, 26, 10, 30),
|
||||||
update_by='user-7',
|
update_by='user-7',
|
||||||
update_time=datetime(2026, 7, 26, 10, 30),
|
update_time=datetime(2026, 7, 26, 10, 30),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -392,6 +402,24 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
|
|||||||
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', make_user(1), TestBatchScope.formal())
|
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', make_user(1), TestBatchScope.formal())
|
||||||
self.assertEqual('托盘库存不存在', raised.exception.message)
|
self.assertEqual('托盘库存不存在', raised.exception.message)
|
||||||
|
|
||||||
|
async def test_test_scope_scan_rejects_cross_batch_pallet(self) -> None:
|
||||||
|
scope = make_test_scope(4)
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
InventoryDao,
|
||||||
|
'get_pallet_by_qr_token',
|
||||||
|
new=AsyncMock(return_value=make_pallet(test_batch_id=5)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_pallet_inventory',
|
||||||
|
new=AsyncMock(return_value=make_inventory(test_batch_id=4)),
|
||||||
|
),
|
||||||
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
|
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||||
|
):
|
||||||
|
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user, scope)
|
||||||
|
|
||||||
|
|
||||||
class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@@ -401,7 +429,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.pallet = make_pallet()
|
self.pallet = make_pallet()
|
||||||
self.inventory = make_inventory()
|
self.inventory = make_inventory()
|
||||||
self.strain = make_strain()
|
self.strain = make_strain()
|
||||||
self.warehouse = SimpleNamespace(warehouse_id=7, status='0')
|
self.warehouse = SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)
|
||||||
self.fixed_time = datetime(2026, 7, 26, 10, 30)
|
self.fixed_time = datetime(2026, 7, 26, 10, 30)
|
||||||
|
|
||||||
async def test_fill_locks_inventory_then_pallet_and_writes_one_atomic_event( # noqa: PLR0915
|
async def test_fill_locks_inventory_then_pallet_and_writes_one_atomic_event( # noqa: PLR0915
|
||||||
@@ -549,6 +577,29 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual('装填成功', result.message)
|
self.assertEqual('装填成功', result.message)
|
||||||
self.assertEqual(31, result.result)
|
self.assertEqual(31, result.result)
|
||||||
|
|
||||||
|
async def test_test_scope_write_rejects_cross_batch_pallet_and_rolls_back(self) -> None:
|
||||||
|
scope = make_test_scope(4)
|
||||||
|
add_filling = AsyncMock()
|
||||||
|
with (
|
||||||
|
patch.object(TestBatchAccessService, 'lock_active_batch', new=AsyncMock()),
|
||||||
|
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
|
||||||
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
|
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=None)),
|
||||||
|
patch.object(
|
||||||
|
InventoryDao,
|
||||||
|
'get_pallet_by_qr_token',
|
||||||
|
new=AsyncMock(return_value=make_pallet(test_batch_id=5)),
|
||||||
|
),
|
||||||
|
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock()),
|
||||||
|
patch.object(OperationDao, 'add_filling_record', new=add_filling),
|
||||||
|
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||||
|
):
|
||||||
|
await FillingService.fill_pallet_services(self.db, self.page, self.user, scope)
|
||||||
|
|
||||||
|
add_filling.assert_not_awaited()
|
||||||
|
self.db.commit.assert_not_awaited()
|
||||||
|
self.db.rollback.assert_awaited_once()
|
||||||
|
|
||||||
async def _assert_fill_rejected(
|
async def _assert_fill_rejected(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -597,7 +648,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
||||||
actions.append('warehouse-read')
|
actions.append('warehouse-read')
|
||||||
return SimpleNamespace(warehouse_id=7, status='1')
|
return SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None)
|
||||||
|
|
||||||
warehouse_read = AsyncMock(side_effect=lock_warehouse)
|
warehouse_read = AsyncMock(side_effect=lock_warehouse)
|
||||||
strain_read = AsyncMock()
|
strain_read = AsyncMock()
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ from module_business.entity.vo.operation_vo import (
|
|||||||
from module_business.service import inbound_service as inbound_service_module
|
from module_business.service import inbound_service as inbound_service_module
|
||||||
from module_business.service.access_service import AccessService
|
from module_business.service.access_service import AccessService
|
||||||
from module_business.service.inbound_service import InboundService
|
from module_business.service.inbound_service import InboundService
|
||||||
|
from tests.module_business.test_batch_fixtures import make_test_scope
|
||||||
|
|
||||||
|
|
||||||
def integrity_error() -> IntegrityError:
|
def integrity_error() -> IntegrityError:
|
||||||
@@ -75,6 +76,7 @@ def make_pallet(pallet_id: int, *, asset_status: str = '0') -> BizPallet:
|
|||||||
qr_token=f'qr-{pallet_id}',
|
qr_token=f'qr-{pallet_id}',
|
||||||
capacity_packages=20,
|
capacity_packages=20,
|
||||||
asset_status=asset_status,
|
asset_status=asset_status,
|
||||||
|
test_batch_id=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +90,7 @@ def make_inventory(
|
|||||||
active_filling_id: int | None = None,
|
active_filling_id: int | None = None,
|
||||||
remaining_packages: int = 0,
|
remaining_packages: int = 0,
|
||||||
version: int = 3,
|
version: int = 3,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizPalletInventory:
|
) -> BizPalletInventory:
|
||||||
return BizPalletInventory(
|
return BizPalletInventory(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
@@ -98,6 +101,7 @@ def make_inventory(
|
|||||||
active_filling_id=active_filling_id,
|
active_filling_id=active_filling_id,
|
||||||
remaining_packages=remaining_packages,
|
remaining_packages=remaining_packages,
|
||||||
version=version,
|
version=version,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +113,7 @@ def make_inbound(
|
|||||||
status: str = 'DRAFT',
|
status: str = 'DRAFT',
|
||||||
idempotency_key: str | None = None,
|
idempotency_key: str | None = None,
|
||||||
request_hash: str | None = None,
|
request_hash: str | None = None,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizInboundOrder:
|
) -> BizInboundOrder:
|
||||||
inbound = BizInboundOrder(
|
inbound = BizInboundOrder(
|
||||||
inbound_id=inbound_id,
|
inbound_id=inbound_id,
|
||||||
@@ -117,12 +122,14 @@ def make_inbound(
|
|||||||
status=status,
|
status=status,
|
||||||
confirm_idempotency_key=idempotency_key,
|
confirm_idempotency_key=idempotency_key,
|
||||||
confirm_request_hash=request_hash,
|
confirm_request_hash=request_hash,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
inbound.items = [
|
inbound.items = [
|
||||||
BizInboundItem(
|
BizInboundItem(
|
||||||
inbound_item_id=index,
|
inbound_item_id=index,
|
||||||
inbound_id=inbound_id,
|
inbound_id=inbound_id,
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
for index, pallet_id in enumerate(pallet_ids, start=1)
|
for index, pallet_id in enumerate(pallet_ids, start=1)
|
||||||
]
|
]
|
||||||
@@ -228,7 +235,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock(side_effect=access)),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock(side_effect=access)),
|
||||||
patch.object(
|
patch.object(
|
||||||
@@ -275,6 +282,32 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertTrue(result.is_success)
|
self.assertTrue(result.is_success)
|
||||||
self.db.rollback.assert_not_awaited()
|
self.db.rollback.assert_not_awaited()
|
||||||
|
|
||||||
|
async def test_add_rejects_cross_batch_locked_inventory_and_rolls_back(self) -> None:
|
||||||
|
inventory = make_inventory(2)
|
||||||
|
inventory.test_batch_id = None
|
||||||
|
pallet = make_pallet(2)
|
||||||
|
pallet.test_batch_id = 4
|
||||||
|
with (
|
||||||
|
patch.object(inbound_service_module.TestBatchAccessService, 'lock_active_batch', new=AsyncMock()),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=4)),
|
||||||
|
),
|
||||||
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
|
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=[inventory])),
|
||||||
|
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[pallet])),
|
||||||
|
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||||
|
):
|
||||||
|
await InboundService.add_inbound_services(
|
||||||
|
self.db,
|
||||||
|
InboundCreateModel(warehouseId=7, palletIds=[2]),
|
||||||
|
self.user,
|
||||||
|
make_test_scope(4),
|
||||||
|
)
|
||||||
|
self.db.rollback.assert_awaited_once()
|
||||||
|
self.db.commit.assert_not_awaited()
|
||||||
|
|
||||||
async def test_add_rejects_invalid_pallet_before_any_order_or_event_write(self) -> None:
|
async def test_add_rejects_invalid_pallet_before_any_order_or_event_write(self) -> None:
|
||||||
page = InboundCreateModel(warehouseId=7, palletIds=[2, 5])
|
page = InboundCreateModel(warehouseId=7, palletIds=[2, 5])
|
||||||
invalid_inventory = make_inventory(5, warehouse_id=7)
|
invalid_inventory = make_inventory(5, warehouse_id=7)
|
||||||
@@ -285,7 +318,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
patch.object(
|
patch.object(
|
||||||
@@ -316,7 +349,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
lock = AsyncMock()
|
lock = AsyncMock()
|
||||||
cases = (
|
cases = (
|
||||||
(None, ServiceException),
|
(None, ServiceException),
|
||||||
(SimpleNamespace(warehouse_id=7, status='1'), ServiceException),
|
(SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None), ServiceException),
|
||||||
)
|
)
|
||||||
for warehouse, exception_type in cases:
|
for warehouse, exception_type in cases:
|
||||||
with self.subTest(warehouse=warehouse):
|
with self.subTest(warehouse=warehouse):
|
||||||
@@ -338,7 +371,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(
|
patch.object(
|
||||||
AccessService,
|
AccessService,
|
||||||
@@ -386,7 +419,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
replace_items = AsyncMock()
|
replace_items = AsyncMock()
|
||||||
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
|
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
|
||||||
warehouse_read = AsyncMock(
|
warehouse_read = AsyncMock(
|
||||||
return_value=SimpleNamespace(warehouse_id=7, status='0'),
|
return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None),
|
||||||
)
|
)
|
||||||
with (
|
with (
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||||
@@ -425,7 +458,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=8, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=8, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(
|
patch.object(
|
||||||
InventoryDao,
|
InventoryDao,
|
||||||
@@ -497,6 +530,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
side_effect=lambda _db, warehouse_id, _scope, **_kwargs: SimpleNamespace(
|
side_effect=lambda _db, warehouse_id, _scope, **_kwargs: SimpleNamespace(
|
||||||
warehouse_id=warehouse_id,
|
warehouse_id=warehouse_id,
|
||||||
status='0',
|
status='0',
|
||||||
|
test_batch_id=None,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -655,7 +689,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
patch.object(
|
patch.object(
|
||||||
@@ -680,7 +714,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=integrity_error())),
|
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=integrity_error())),
|
||||||
@@ -830,9 +864,9 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_warehouse_options_filter_disabled_and_unauthorized_records(self) -> None:
|
async def test_warehouse_options_filter_disabled_and_unauthorized_records(self) -> None:
|
||||||
warehouses = [
|
warehouses = [
|
||||||
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0'),
|
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0', test_batch_id=None),
|
||||||
SimpleNamespace(warehouse_id=3, warehouse_code='W3', warehouse_name='三仓', status='1'),
|
SimpleNamespace(warehouse_id=3, warehouse_code='W3', warehouse_name='三仓', status='1', test_batch_id=None),
|
||||||
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0'),
|
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0', test_batch_id=None),
|
||||||
]
|
]
|
||||||
with (
|
with (
|
||||||
patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)) as list_warehouse,
|
patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)) as list_warehouse,
|
||||||
@@ -849,8 +883,8 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_admin_warehouse_options_include_all_active_records(self) -> None:
|
async def test_admin_warehouse_options_include_all_active_records(self) -> None:
|
||||||
warehouses = [
|
warehouses = [
|
||||||
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0'),
|
SimpleNamespace(warehouse_id=2, warehouse_code='W2', warehouse_name='二仓', status='0', test_batch_id=None),
|
||||||
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0'),
|
SimpleNamespace(warehouse_id=5, warehouse_code='W5', warehouse_name='五仓', status='0', test_batch_id=None),
|
||||||
]
|
]
|
||||||
scope = AsyncMock()
|
scope = AsyncMock()
|
||||||
with (
|
with (
|
||||||
@@ -894,7 +928,7 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=pallet)) as get_pallet,
|
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=pallet)) as get_pallet,
|
||||||
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=inventory)) as get_inventory,
|
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=inventory)) as get_inventory,
|
||||||
@@ -938,7 +972,7 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=pallet)),
|
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=pallet)),
|
||||||
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=inventory)),
|
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=inventory)),
|
||||||
@@ -969,7 +1003,7 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
get_pallet.assert_not_awaited()
|
get_pallet.assert_not_awaited()
|
||||||
|
|
||||||
for warehouse in (None, SimpleNamespace(warehouse_id=7, status='1')):
|
for warehouse in (None, SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None)):
|
||||||
with self.subTest(warehouse=warehouse):
|
with self.subTest(warehouse=warehouse):
|
||||||
get_pallet.reset_mock()
|
get_pallet.reset_mock()
|
||||||
with (
|
with (
|
||||||
@@ -1079,7 +1113,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
||||||
actions.append('warehouse-lock')
|
actions.append('warehouse-lock')
|
||||||
return SimpleNamespace(warehouse_id=7, status='0')
|
return SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)
|
||||||
|
|
||||||
def transition(current: PalletFlowState, target: PalletFlowState) -> None:
|
def transition(current: PalletFlowState, target: PalletFlowState) -> None:
|
||||||
actions.append(f'transition-{current.value}-{target.value}')
|
actions.append(f'transition-{current.value}-{target.value}')
|
||||||
@@ -1282,7 +1316,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
) as warehouse_lock,
|
) as warehouse_lock,
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||||
patch.object(OperationDao, 'update_inbound', new=update),
|
patch.object(OperationDao, 'update_inbound', new=update),
|
||||||
@@ -1343,7 +1377,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(PalletStateMachine, 'ensure_transition', side_effect=ValueError('invalid')),
|
patch.object(PalletStateMachine, 'ensure_transition', side_effect=ValueError('invalid')),
|
||||||
patch.object(OperationDao, 'update_inbound', new=update),
|
patch.object(OperationDao, 'update_inbound', new=update),
|
||||||
@@ -1378,7 +1412,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
) as warehouse_lock,
|
) as warehouse_lock,
|
||||||
):
|
):
|
||||||
result = await InboundService.confirm_inbound_services(
|
result = await InboundService.confirm_inbound_services(
|
||||||
@@ -1439,7 +1473,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
):
|
):
|
||||||
@@ -1486,7 +1520,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='1')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='1', test_batch_id=None)),
|
||||||
) as warehouse_lock,
|
) as warehouse_lock,
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||||
self.assertRaises(PermissionException) as raised,
|
self.assertRaises(PermissionException) as raised,
|
||||||
@@ -1564,7 +1598,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||||
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
||||||
@@ -1600,7 +1634,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||||
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
||||||
@@ -1674,7 +1708,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
):
|
):
|
||||||
@@ -1737,7 +1771,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
) as warehouse_lock,
|
) as warehouse_lock,
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
|
||||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||||
@@ -1777,7 +1811,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(
|
patch.object(
|
||||||
MasterDao,
|
MasterDao,
|
||||||
'get_warehouse',
|
'get_warehouse',
|
||||||
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
|
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0', test_batch_id=None)),
|
||||||
),
|
),
|
||||||
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
|
||||||
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ from module_business.entity.vo.operation_vo import (
|
|||||||
)
|
)
|
||||||
from module_business.service.access_service import AccessService
|
from module_business.service.access_service import AccessService
|
||||||
from module_business.service.test_batch_access_service import TestBatchAccessService
|
from module_business.service.test_batch_access_service import TestBatchAccessService
|
||||||
|
from tests.module_business.test_batch_fixtures import make_test_scope
|
||||||
|
|
||||||
RETURN_SERVICE_SPEC = importlib.util.find_spec('module_business.service.return_service')
|
RETURN_SERVICE_SPEC = importlib.util.find_spec('module_business.service.return_service')
|
||||||
RETURN_CONTROLLER_SPEC = importlib.util.find_spec('module_business.controller.return_controller')
|
RETURN_CONTROLLER_SPEC = importlib.util.find_spec('module_business.controller.return_controller')
|
||||||
@@ -70,13 +71,20 @@ def make_user(user_id: int = 7) -> CurrentUserModel:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_pallet(pallet_id: int, *, capacity: int = 20, asset_status: str = '0') -> BizPallet:
|
def make_pallet(
|
||||||
|
pallet_id: int,
|
||||||
|
*,
|
||||||
|
capacity: int = 20,
|
||||||
|
asset_status: str = '0',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
|
) -> BizPallet:
|
||||||
return BizPallet(
|
return BizPallet(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
pallet_code=f'P-{pallet_id:03d}',
|
pallet_code=f'P-{pallet_id:03d}',
|
||||||
qr_token=f'qr-{pallet_id}',
|
qr_token=f'qr-{pallet_id}',
|
||||||
capacity_packages=capacity,
|
capacity_packages=capacity,
|
||||||
asset_status=asset_status,
|
asset_status=asset_status,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +98,7 @@ def make_inventory(
|
|||||||
filling_id: int | None = -1,
|
filling_id: int | None = -1,
|
||||||
state: str = PalletFlowState.AT_SITE_PARTIAL.value,
|
state: str = PalletFlowState.AT_SITE_PARTIAL.value,
|
||||||
version: int = 3,
|
version: int = 3,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizPalletInventory:
|
) -> BizPalletInventory:
|
||||||
active_filling_id = (100 + pallet_id if remaining > 0 else None) if filling_id == -1 else filling_id
|
active_filling_id = (100 + pallet_id if remaining > 0 else None) if filling_id == -1 else filling_id
|
||||||
return BizPalletInventory(
|
return BizPalletInventory(
|
||||||
@@ -101,10 +110,16 @@ def make_inventory(
|
|||||||
remaining_packages=remaining,
|
remaining_packages=remaining,
|
||||||
flow_state=state,
|
flow_state=state,
|
||||||
version=version,
|
version=version,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_filling(pallet_id: int, *, warehouse_id: int = 3) -> BizFillingRecord:
|
def make_filling(
|
||||||
|
pallet_id: int,
|
||||||
|
*,
|
||||||
|
warehouse_id: int = 3,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
|
) -> BizFillingRecord:
|
||||||
return BizFillingRecord(
|
return BizFillingRecord(
|
||||||
filling_id=100 + pallet_id,
|
filling_id=100 + pallet_id,
|
||||||
filling_no=f'FI-{pallet_id}',
|
filling_no=f'FI-{pallet_id}',
|
||||||
@@ -121,6 +136,7 @@ def make_filling(pallet_id: int, *, warehouse_id: int = 3) -> BizFillingRecord:
|
|||||||
request_hash='f' * 64,
|
request_hash='f' * 64,
|
||||||
operator_id=7,
|
operator_id=7,
|
||||||
operated_at=datetime(2026, 7, 1),
|
operated_at=datetime(2026, 7, 1),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -133,6 +149,7 @@ def make_return_order(
|
|||||||
request_hash: str = 'r' * 64,
|
request_hash: str = 'r' * 64,
|
||||||
confirm_key: str | None = None,
|
confirm_key: str | None = None,
|
||||||
confirm_hash: str | None = None,
|
confirm_hash: str | None = None,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizReturnOrder:
|
) -> BizReturnOrder:
|
||||||
order = BizReturnOrder(
|
order = BizReturnOrder(
|
||||||
return_id=return_id,
|
return_id=return_id,
|
||||||
@@ -146,6 +163,7 @@ def make_return_order(
|
|||||||
confirm_request_hash=confirm_hash,
|
confirm_request_hash=confirm_hash,
|
||||||
requested_by=8,
|
requested_by=8,
|
||||||
requested_at=datetime(2026, 7, 25, 9),
|
requested_at=datetime(2026, 7, 25, 9),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
order.items = [
|
order.items = [
|
||||||
BizReturnItem(
|
BizReturnItem(
|
||||||
@@ -154,6 +172,7 @@ def make_return_order(
|
|||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
active_filling_id_snapshot=filling_id,
|
active_filling_id_snapshot=filling_id,
|
||||||
remaining_packages_snapshot=remaining,
|
remaining_packages_snapshot=remaining,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
for index, (pallet_id, filling_id, remaining) in enumerate(snapshots, start=1)
|
for index, (pallet_id, filling_id, remaining) in enumerate(snapshots, start=1)
|
||||||
]
|
]
|
||||||
@@ -573,6 +592,35 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(8, actual.remaining_packages)
|
self.assertEqual(8, actual.remaining_packages)
|
||||||
self.assertEqual(['request'], actual.allowed_actions)
|
self.assertEqual(['request'], actual.allowed_actions)
|
||||||
|
|
||||||
|
async def test_test_scope_scan_rejects_cross_batch_pallet(self) -> None:
|
||||||
|
scope = make_test_scope(4)
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
InventoryDao,
|
||||||
|
'get_pallet_by_qr_token',
|
||||||
|
new=AsyncMock(return_value=make_pallet(2, test_batch_id=5)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_pallet_inventory',
|
||||||
|
new=AsyncMock(return_value=make_inventory(2, test_batch_id=4)),
|
||||||
|
),
|
||||||
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
|
patch.object(
|
||||||
|
OperationDao,
|
||||||
|
'get_filling_by_id',
|
||||||
|
new=AsyncMock(return_value=make_filling(2, test_batch_id=4)),
|
||||||
|
),
|
||||||
|
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||||
|
):
|
||||||
|
await ReturnService.scan_return_pallet_services(
|
||||||
|
self.db,
|
||||||
|
'qr-2',
|
||||||
|
ReturnScanQueryModel(mode='request'),
|
||||||
|
self.user,
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
|
||||||
async def test_confirm_scan_is_scoped_to_target_warehouse_and_exact_item_snapshot(self) -> None:
|
async def test_confirm_scan_is_scoped_to_target_warehouse_and_exact_item_snapshot(self) -> None:
|
||||||
order = make_return_order(snapshots=((2, 102, 8),))
|
order = make_return_order(snapshots=((2, 102, 8),))
|
||||||
pallet = make_pallet(2)
|
pallet = make_pallet(2)
|
||||||
@@ -687,11 +735,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
||||||
actions.append('warehouse-row')
|
actions.append('warehouse-row')
|
||||||
return SimpleNamespace(status='0')
|
return SimpleNamespace(status='0', test_batch_id=None)
|
||||||
|
|
||||||
async def lock_site(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
async def lock_site(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
||||||
actions.append('site-row')
|
actions.append('site-row')
|
||||||
return SimpleNamespace(status='0')
|
return SimpleNamespace(status='0', test_batch_id=None)
|
||||||
|
|
||||||
async def lock_site_mapping(*_args: Any, **_kwargs: Any) -> None:
|
async def lock_site_mapping(*_args: Any, **_kwargs: Any) -> None:
|
||||||
actions.append('site-mapping')
|
actions.append('site-mapping')
|
||||||
@@ -772,8 +820,16 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
),
|
),
|
||||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_access),
|
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_access),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
patch.object(OperationDao, 'add_return_order', new=AsyncMock(return_value=probe)),
|
patch.object(OperationDao, 'add_return_order', new=AsyncMock(return_value=probe)),
|
||||||
patch.object(OperationDao, 'add_return_items', new=AsyncMock(side_effect=add_items)),
|
patch.object(OperationDao, 'add_return_items', new=AsyncMock(side_effect=add_items)),
|
||||||
patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=add_event)),
|
patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=add_event)),
|
||||||
@@ -822,8 +878,16 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(OperationDao, 'get_active_filling_for_update', new=AsyncMock(return_value=filling)),
|
patch.object(OperationDao, 'get_active_filling_for_update', new=AsyncMock(return_value=filling)),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
||||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=4)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=4)),
|
||||||
|
),
|
||||||
patch.object(OperationDao, 'add_return_order', new=AsyncMock(side_effect=integrity_error())),
|
patch.object(OperationDao, 'add_return_order', new=AsyncMock(side_effect=integrity_error())),
|
||||||
):
|
):
|
||||||
result = await ReturnService.request_return_services(self.db, self.page, self.user, scope)
|
result = await ReturnService.request_return_services(self.db, self.page, self.user, scope)
|
||||||
@@ -886,7 +950,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def lock_site(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
async def lock_site(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
|
||||||
actions.append('site-lock')
|
actions.append('site-lock')
|
||||||
return SimpleNamespace(status='0')
|
return SimpleNamespace(status='0', test_batch_id=None)
|
||||||
|
|
||||||
key_read = AsyncMock(side_effect=read_key)
|
key_read = AsyncMock(side_effect=read_key)
|
||||||
scope_read = AsyncMock(side_effect=read_scope)
|
scope_read = AsyncMock(side_effect=read_scope)
|
||||||
@@ -1010,7 +1074,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=relocated)),
|
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=relocated)),
|
||||||
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_locks),
|
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_locks),
|
||||||
patch.object(OperationDao, 'get_active_filling_for_update', new=filling_locks),
|
patch.object(OperationDao, 'get_active_filling_for_update', new=filling_locks),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order,
|
patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order,
|
||||||
patch.object(InventoryDao, 'add_event', new=AsyncMock()) as add_event,
|
patch.object(InventoryDao, 'add_event', new=AsyncMock()) as add_event,
|
||||||
):
|
):
|
||||||
@@ -1105,7 +1173,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
'get_active_filling_for_update',
|
'get_active_filling_for_update',
|
||||||
new=AsyncMock(return_value=make_filling(2)),
|
new=AsyncMock(return_value=make_filling(2)),
|
||||||
),
|
),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
):
|
):
|
||||||
result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
||||||
|
|
||||||
@@ -1192,7 +1264,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
new=AsyncMock(return_value=probe),
|
new=AsyncMock(return_value=probe),
|
||||||
),
|
),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as inventory_locks,
|
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as inventory_locks,
|
||||||
):
|
):
|
||||||
result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
||||||
@@ -1271,8 +1347,16 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
'get_active_filling_for_update',
|
'get_active_filling_for_update',
|
||||||
new=AsyncMock(return_value=make_filling(5)),
|
new=AsyncMock(return_value=make_filling(5)),
|
||||||
),
|
),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
||||||
@@ -1480,7 +1564,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
),
|
),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
||||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks,
|
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks,
|
||||||
):
|
):
|
||||||
result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
|
||||||
@@ -1567,7 +1655,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[10, 9])),
|
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[10, 9])),
|
||||||
patch.object(OperationDao, 'get_return_by_request_idempotency_key', new=key_read),
|
patch.object(OperationDao, 'get_return_by_request_idempotency_key', new=key_read),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
):
|
):
|
||||||
result = await ReturnService._recover_request_integrity_error(
|
result = await ReturnService._recover_request_integrity_error(
|
||||||
@@ -1604,7 +1696,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(OperationDao, 'get_return_by_request_idempotency_key', new=get_by_key),
|
patch.object(OperationDao, 'get_return_by_request_idempotency_key', new=get_by_key),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
||||||
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
):
|
):
|
||||||
result = await ReturnService.request_return_services(self.db, self.page, admin, TestBatchScope.formal())
|
result = await ReturnService.request_return_services(self.db, self.page, admin, TestBatchScope.formal())
|
||||||
|
|
||||||
@@ -1822,7 +1918,11 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
),
|
),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks,
|
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks,
|
||||||
):
|
):
|
||||||
result = await ReturnService.confirm_return_services(
|
result = await ReturnService.confirm_return_services(
|
||||||
@@ -1871,7 +1971,11 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
),
|
),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
patch.object(OperationDao, 'get_return_for_update', new=AsyncMock(return_value=exact)),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
):
|
):
|
||||||
result = await ReturnService.confirm_return_services(self.db, 13, self.page, admin, TestBatchScope.formal())
|
result = await ReturnService.confirm_return_services(self.db, 13, self.page, admin, TestBatchScope.formal())
|
||||||
|
|
||||||
@@ -1916,7 +2020,11 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
patch.object(OperationDao, 'get_return_by_id', new=route_read),
|
patch.object(OperationDao, 'get_return_by_id', new=route_read),
|
||||||
patch.object(OperationDao, 'get_return_by_confirm_idempotency_key', new=key_read),
|
patch.object(OperationDao, 'get_return_by_confirm_idempotency_key', new=key_read),
|
||||||
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
patch.object(OperationDao, 'get_return_for_update', new=order_lock),
|
||||||
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(return_value=SimpleNamespace(status='0', test_batch_id=None)),
|
||||||
|
),
|
||||||
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
):
|
):
|
||||||
result = await ReturnService._recover_confirm_integrity_error(
|
result = await ReturnService._recover_confirm_integrity_error(
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ from module_business.entity.vo.operation_vo import (
|
|||||||
from module_business.service import shipment_service as shipment_service_module
|
from module_business.service import shipment_service as shipment_service_module
|
||||||
from module_business.service.access_service import AccessService
|
from module_business.service.access_service import AccessService
|
||||||
from module_business.service.shipment_service import ShipmentService
|
from module_business.service.shipment_service import ShipmentService
|
||||||
|
from module_business.service.test_batch_access_service import TestBatchAccessService
|
||||||
|
from tests.module_business.test_batch_fixtures import make_test_scope
|
||||||
|
|
||||||
|
|
||||||
def make_user(user_id: int = 7) -> CurrentUserModel:
|
def make_user(user_id: int = 7) -> CurrentUserModel:
|
||||||
@@ -69,13 +71,20 @@ def integrity_error() -> IntegrityError:
|
|||||||
return IntegrityError('statement', {}, RuntimeError('constraint'))
|
return IntegrityError('statement', {}, RuntimeError('constraint'))
|
||||||
|
|
||||||
|
|
||||||
def make_pallet(pallet_id: int, *, capacity: int = 20, asset_status: str = '0') -> BizPallet:
|
def make_pallet(
|
||||||
|
pallet_id: int,
|
||||||
|
*,
|
||||||
|
capacity: int = 20,
|
||||||
|
asset_status: str = '0',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
|
) -> BizPallet:
|
||||||
return BizPallet(
|
return BizPallet(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
pallet_code=f'P-{pallet_id:03d}',
|
pallet_code=f'P-{pallet_id:03d}',
|
||||||
qr_token=f'qr-{pallet_id}',
|
qr_token=f'qr-{pallet_id}',
|
||||||
capacity_packages=capacity,
|
capacity_packages=capacity,
|
||||||
asset_status=asset_status,
|
asset_status=asset_status,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -89,6 +98,7 @@ def make_inventory(
|
|||||||
remaining_packages: int = 8,
|
remaining_packages: int = 8,
|
||||||
flow_state: str = 'FILLED_IN_WAREHOUSE',
|
flow_state: str = 'FILLED_IN_WAREHOUSE',
|
||||||
version: int = 3,
|
version: int = 3,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizPalletInventory:
|
) -> BizPalletInventory:
|
||||||
return BizPalletInventory(
|
return BizPalletInventory(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
@@ -99,10 +109,16 @@ def make_inventory(
|
|||||||
remaining_packages=remaining_packages,
|
remaining_packages=remaining_packages,
|
||||||
flow_state=flow_state,
|
flow_state=flow_state,
|
||||||
version=version,
|
version=version,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_filling(pallet_id: int, *, warehouse_id: int = 7) -> BizFillingRecord:
|
def make_filling(
|
||||||
|
pallet_id: int,
|
||||||
|
*,
|
||||||
|
warehouse_id: int = 7,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
|
) -> BizFillingRecord:
|
||||||
return BizFillingRecord(
|
return BizFillingRecord(
|
||||||
filling_id=100 + pallet_id,
|
filling_id=100 + pallet_id,
|
||||||
filling_no=f'FI-{pallet_id}',
|
filling_no=f'FI-{pallet_id}',
|
||||||
@@ -119,6 +135,7 @@ def make_filling(pallet_id: int, *, warehouse_id: int = 7) -> BizFillingRecord:
|
|||||||
request_hash='f' * 64,
|
request_hash='f' * 64,
|
||||||
operator_id=7,
|
operator_id=7,
|
||||||
operated_at=SimpleNamespace(),
|
operated_at=SimpleNamespace(),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -131,6 +148,7 @@ def make_shipment(
|
|||||||
status: str = 'DRAFT',
|
status: str = 'DRAFT',
|
||||||
idempotency_key: str | None = None,
|
idempotency_key: str | None = None,
|
||||||
request_hash: str | None = None,
|
request_hash: str | None = None,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizShipmentOrder:
|
) -> BizShipmentOrder:
|
||||||
shipment = BizShipmentOrder(
|
shipment = BizShipmentOrder(
|
||||||
shipment_id=shipment_id,
|
shipment_id=shipment_id,
|
||||||
@@ -140,12 +158,14 @@ def make_shipment(
|
|||||||
status=status,
|
status=status,
|
||||||
confirm_idempotency_key=idempotency_key,
|
confirm_idempotency_key=idempotency_key,
|
||||||
confirm_request_hash=request_hash,
|
confirm_request_hash=request_hash,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
shipment.items = [
|
shipment.items = [
|
||||||
BizShipmentItem(
|
BizShipmentItem(
|
||||||
shipment_item_id=index,
|
shipment_item_id=index,
|
||||||
shipment_id=shipment_id,
|
shipment_id=shipment_id,
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
for index, pallet_id in enumerate(pallet_ids, start=1)
|
for index, pallet_id in enumerate(pallet_ids, start=1)
|
||||||
]
|
]
|
||||||
@@ -756,6 +776,58 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual('无权访问该托盘', raised.exception.message)
|
self.assertEqual('无权访问该托盘', raised.exception.message)
|
||||||
|
|
||||||
|
async def test_test_scope_scan_rejects_cross_batch_pallet(self) -> None:
|
||||||
|
scope = make_test_scope(4)
|
||||||
|
query = ShipmentScanQueryModel(sourceWarehouseId=7, targetSiteId=9)
|
||||||
|
with (
|
||||||
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(
|
||||||
|
return_value=BizWarehouse(
|
||||||
|
warehouse_id=7,
|
||||||
|
warehouse_code='W-7',
|
||||||
|
warehouse_name='七号仓',
|
||||||
|
status='0',
|
||||||
|
test_batch_id=4,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(
|
||||||
|
return_value=BizCustomerSite(
|
||||||
|
site_id=9,
|
||||||
|
customer_id=1,
|
||||||
|
site_code='S-9',
|
||||||
|
site_name='九号现场',
|
||||||
|
status='0',
|
||||||
|
test_batch_id=4,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
InventoryDao,
|
||||||
|
'get_pallet_by_qr_token',
|
||||||
|
new=AsyncMock(return_value=make_pallet(2, test_batch_id=5)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_pallet_inventory',
|
||||||
|
new=AsyncMock(return_value=make_inventory(2, test_batch_id=4)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
OperationDao,
|
||||||
|
'get_filling_by_id',
|
||||||
|
new=AsyncMock(return_value=make_filling(2, test_batch_id=4)),
|
||||||
|
),
|
||||||
|
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||||
|
):
|
||||||
|
await ShipmentService.scan_shipment_pallet_services(self.db, 'qr-2', query, self.user, scope)
|
||||||
|
|
||||||
|
|
||||||
class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
|
class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@@ -792,6 +864,58 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual([pallet], pallets)
|
self.assertEqual([pallet], pallets)
|
||||||
self.assertEqual([origin_filling], fillings)
|
self.assertEqual([origin_filling], fillings)
|
||||||
|
|
||||||
|
async def test_test_scope_write_rejects_cross_batch_inventory_and_rolls_back(self) -> None:
|
||||||
|
scope = make_test_scope(4)
|
||||||
|
page = ShipmentCreateModel(sourceWarehouseId=7, targetSiteId=9, palletIds=[2])
|
||||||
|
add_shipment = AsyncMock()
|
||||||
|
with (
|
||||||
|
patch.object(TestBatchAccessService, 'lock_active_batch', new=AsyncMock()),
|
||||||
|
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
|
||||||
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_warehouse',
|
||||||
|
new=AsyncMock(
|
||||||
|
return_value=BizWarehouse(
|
||||||
|
warehouse_id=7,
|
||||||
|
warehouse_code='W-7',
|
||||||
|
warehouse_name='七号仓',
|
||||||
|
status='0',
|
||||||
|
test_batch_id=4,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_site',
|
||||||
|
new=AsyncMock(
|
||||||
|
return_value=BizCustomerSite(
|
||||||
|
site_id=9,
|
||||||
|
customer_id=1,
|
||||||
|
site_code='S-9',
|
||||||
|
site_name='九号现场',
|
||||||
|
status='0',
|
||||||
|
test_batch_id=4,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
InventoryDao,
|
||||||
|
'get_inventories_for_update',
|
||||||
|
new=AsyncMock(return_value=[make_inventory(2, test_batch_id=5)]),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[make_pallet(2, test_batch_id=4)])
|
||||||
|
),
|
||||||
|
patch.object(OperationDao, 'add_shipment', new=add_shipment),
|
||||||
|
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||||
|
):
|
||||||
|
await ShipmentService.add_shipment_services(self.db, page, self.user, scope)
|
||||||
|
|
||||||
|
add_shipment.assert_not_awaited()
|
||||||
|
self.db.commit.assert_not_awaited()
|
||||||
|
self.db.rollback.assert_awaited_once()
|
||||||
|
|
||||||
async def test_add_only_persists_pallet_ids_with_null_snapshots(self) -> None: # noqa: PLR0915
|
async def test_add_only_persists_pallet_ids_with_null_snapshots(self) -> None: # noqa: PLR0915
|
||||||
page = ShipmentCreateModel(sourceWarehouseId=7, targetSiteId=9, palletIds=[5, 2])
|
page = ShipmentCreateModel(sourceWarehouseId=7, targetSiteId=9, palletIds=[5, 2])
|
||||||
inventories = [make_inventory(2), make_inventory(5)]
|
inventories = [make_inventory(2), make_inventory(5)]
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ def make_pallet(
|
|||||||
qr_token: str = 'qr-2',
|
qr_token: str = 'qr-2',
|
||||||
capacity_packages: int = 20,
|
capacity_packages: int = 20,
|
||||||
asset_status: str = '0',
|
asset_status: str = '0',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizPallet:
|
) -> BizPallet:
|
||||||
return BizPallet(
|
return BizPallet(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
@@ -107,6 +108,7 @@ def make_pallet(
|
|||||||
qr_token=qr_token,
|
qr_token=qr_token,
|
||||||
capacity_packages=capacity_packages,
|
capacity_packages=capacity_packages,
|
||||||
asset_status=asset_status,
|
asset_status=asset_status,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ def make_inventory(
|
|||||||
remaining_packages: int = 8,
|
remaining_packages: int = 8,
|
||||||
flow_state: str = PalletFlowState.AT_SITE_UNUSED.value,
|
flow_state: str = PalletFlowState.AT_SITE_UNUSED.value,
|
||||||
version: int = 3,
|
version: int = 3,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizPalletInventory:
|
) -> BizPalletInventory:
|
||||||
return BizPalletInventory(
|
return BizPalletInventory(
|
||||||
pallet_id=pallet_id,
|
pallet_id=pallet_id,
|
||||||
@@ -131,6 +134,7 @@ def make_inventory(
|
|||||||
last_event_id=40,
|
last_event_id=40,
|
||||||
version=version,
|
version=version,
|
||||||
updated_at=datetime(2026, 7, 25, 9, 0),
|
updated_at=datetime(2026, 7, 25, 9, 0),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -139,6 +143,7 @@ def make_filling(
|
|||||||
*,
|
*,
|
||||||
pallet_id: int = 2,
|
pallet_id: int = 2,
|
||||||
active_slot: str | None = 'ACTIVE',
|
active_slot: str | None = 'ACTIVE',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizFillingRecord:
|
) -> BizFillingRecord:
|
||||||
return BizFillingRecord(
|
return BizFillingRecord(
|
||||||
filling_id=filling_id,
|
filling_id=filling_id,
|
||||||
@@ -157,16 +162,23 @@ def make_filling(
|
|||||||
request_hash='f' * 64,
|
request_hash='f' * 64,
|
||||||
operator_id=7,
|
operator_id=7,
|
||||||
operated_at=datetime(2026, 7, 25, 8, 0),
|
operated_at=datetime(2026, 7, 25, 8, 0),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_site(site_id: int = 9, *, status: str = '0') -> BizCustomerSite:
|
def make_site(
|
||||||
|
site_id: int = 9,
|
||||||
|
*,
|
||||||
|
status: str = '0',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
|
) -> BizCustomerSite:
|
||||||
return BizCustomerSite(
|
return BizCustomerSite(
|
||||||
site_id=site_id,
|
site_id=site_id,
|
||||||
customer_id=3,
|
customer_id=3,
|
||||||
site_code=f'SITE-{site_id}',
|
site_code=f'SITE-{site_id}',
|
||||||
site_name=f'现场 {site_id}',
|
site_name=f'现场 {site_id}',
|
||||||
status=status,
|
status=status,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -175,6 +187,7 @@ def make_line(
|
|||||||
*,
|
*,
|
||||||
site_id: int = 9,
|
site_id: int = 9,
|
||||||
status: str = '0',
|
status: str = '0',
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizProductionLine:
|
) -> BizProductionLine:
|
||||||
return BizProductionLine(
|
return BizProductionLine(
|
||||||
line_id=line_id,
|
line_id=line_id,
|
||||||
@@ -182,6 +195,7 @@ def make_line(
|
|||||||
line_code=f'L-{line_id}',
|
line_code=f'L-{line_id}',
|
||||||
line_name=f'产线 {line_id}',
|
line_name=f'产线 {line_id}',
|
||||||
status=status,
|
status=status,
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -192,6 +206,7 @@ def make_usage(
|
|||||||
line_id: int = 4,
|
line_id: int = 4,
|
||||||
idempotency_key: str = 'usage-key',
|
idempotency_key: str = 'usage-key',
|
||||||
request_hash: str = 'a' * 64,
|
request_hash: str = 'a' * 64,
|
||||||
|
test_batch_id: int | None = None,
|
||||||
) -> BizUsageRecord:
|
) -> BizUsageRecord:
|
||||||
return BizUsageRecord(
|
return BizUsageRecord(
|
||||||
usage_id=usage_id,
|
usage_id=usage_id,
|
||||||
@@ -206,6 +221,7 @@ def make_usage(
|
|||||||
request_hash=request_hash,
|
request_hash=request_hash,
|
||||||
operator_id=7,
|
operator_id=7,
|
||||||
occurred_at=datetime(2026, 7, 26, 11, 30),
|
occurred_at=datetime(2026, 7, 26, 11, 30),
|
||||||
|
test_batch_id=test_batch_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -519,6 +535,29 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(5, result.remaining_packages)
|
self.assertEqual(5, result.remaining_packages)
|
||||||
self.assertEqual(['usage'], result.allowed_actions)
|
self.assertEqual(['usage'], result.allowed_actions)
|
||||||
|
|
||||||
|
async def test_test_scope_scan_rejects_cross_batch_pallet(self) -> None:
|
||||||
|
scope = make_test_scope(4)
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
InventoryDao,
|
||||||
|
'get_pallet_by_qr_token',
|
||||||
|
new=AsyncMock(return_value=make_pallet(test_batch_id=5)),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
MasterDao,
|
||||||
|
'get_pallet_inventory',
|
||||||
|
new=AsyncMock(return_value=make_inventory(test_batch_id=4)),
|
||||||
|
),
|
||||||
|
patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
|
||||||
|
patch.object(
|
||||||
|
OperationDao,
|
||||||
|
'get_filling_by_id',
|
||||||
|
new=AsyncMock(return_value=make_filling(test_batch_id=4)),
|
||||||
|
),
|
||||||
|
self.assertRaisesRegex(ServiceException, '不属于当前数据模式或测试批次'),
|
||||||
|
):
|
||||||
|
await UsageService.scan_usage_pallet_services(self.db, 'qr-2', self.user, scope)
|
||||||
|
|
||||||
async def test_scan_hides_missing_non_site_and_unauthorized_qr_for_normal_user(self) -> None:
|
async def test_scan_hides_missing_non_site_and_unauthorized_qr_for_normal_user(self) -> None:
|
||||||
with (
|
with (
|
||||||
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
|
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
|
||||||
@@ -1683,6 +1722,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.expired = False
|
self.expired = False
|
||||||
self.pallet_id_reads = 0
|
self.pallet_id_reads = 0
|
||||||
self._pallet_id = pallet_id
|
self._pallet_id = pallet_id
|
||||||
|
self.test_batch_id = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pallet_id(self) -> int:
|
def pallet_id(self) -> int:
|
||||||
|
|||||||
Reference in New Issue
Block a user