修复:收紧托盘业务权限与数据契约

This commit is contained in:
焦龙言
2026-07-27 17:40:24 +08:00
parent 9b1a807098
commit 4a321098f8
16 changed files with 823 additions and 59 deletions
@@ -178,7 +178,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
actions: list[str] = []
captured: dict[str, Any] = {}
async def access(*_args: Any) -> None:
async def access(*_args: Any, **_kwargs: Any) -> None:
actions.append('access')
async def lock_inventories(*_args: Any) -> list[BizPalletInventory]:
@@ -231,7 +231,15 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
result = await InboundService.add_inbound_services(self.db, page, self.user)
self.assertEqual(
['access', 'inventory-lock', 'pallet-lock', 'add-order', 'replace-items', 'commit'],
[
'access',
'inventory-lock',
'pallet-lock',
'access',
'add-order',
'replace-items',
'commit',
],
actions,
)
inventory_lock.assert_awaited_once_with(self.db, [2, 5])
@@ -352,6 +360,43 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
pallet_lock.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_add_rechecks_revoked_mapping_after_pallet_locks_before_writing(self) -> None:
page = InboundCreateModel(warehouseId=7, palletIds=[2])
async def warehouse_access(*_args: Any, **kwargs: Any) -> None:
if kwargs.get('for_update'):
raise PermissionException(data='', message='无权访问该仓库')
add_order = AsyncMock(return_value=make_inbound(pallet_ids=(2,)))
replace_items = AsyncMock()
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
warehouse_read = AsyncMock(
return_value=SimpleNamespace(warehouse_id=7, status='0'),
)
with (
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
patch.object(MasterDao, 'get_warehouse', new=warehouse_read),
patch.object(
InboundService,
'_lock_and_validate_pallets',
new=AsyncMock(return_value=([make_inventory(2)], [make_pallet(2)])),
),
patch.object(OperationDao, 'add_inbound', new=add_order),
patch.object(OperationDao, 'replace_inbound_items', new=replace_items),
self.assertRaises(PermissionException) as raised,
):
await InboundService.add_inbound_services(self.db, page, self.user)
self.assertEqual('无权访问该仓库', raised.exception.message)
self.assertEqual(2, warehouse_read.await_count)
warehouse_read.assert_awaited_with(self.db, 7, for_update=True)
self.assertEqual(2, warehouse_mapping.await_count)
warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True)
add_order.assert_not_awaited()
replace_items.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_edit_scopes_original_and_checks_target_access_then_replaces_items(self) -> None:
page = InboundUpdateModel(inboundId=11, warehouseId=8, palletIds=[5, 2])
inbound = make_inbound(warehouse_id=7, pallet_ids=(3,))
@@ -390,7 +435,14 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
11,
warehouse_ids=[2, 5, 7, 8, 11],
)
access.assert_awaited_once_with(self.db, self.user, 8)
self.assertEqual(
[
unittest.mock.call(self.db, self.user, 8),
unittest.mock.call(self.db, self.user, 7, for_update=True),
unittest.mock.call(self.db, self.user, 8, for_update=True),
],
access.await_args_list,
)
inventory_lock.assert_awaited_once_with(self.db, [2, 5])
pallet_lock.assert_awaited_once_with(self.db, [2, 5])
self.assertEqual(8, inbound.warehouse_id)
@@ -402,6 +454,55 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
self.db.commit.assert_awaited_once()
self.assertTrue(result.is_success)
async def test_edit_hides_document_when_current_mapping_is_revoked_after_pallet_locks(self) -> None:
current_warehouse_id = 7
page = InboundUpdateModel(inboundId=11, warehouseId=8, palletIds=[2])
inbound = make_inbound(warehouse_id=current_warehouse_id, pallet_ids=(2,))
async def warehouse_access(
_db: AsyncSession,
_user: CurrentUserModel,
warehouse_id: int,
**kwargs: Any,
) -> None:
if warehouse_id == current_warehouse_id and kwargs.get('for_update'):
raise PermissionException(data='', message='无权访问该仓库')
update = AsyncMock()
replace = AsyncMock()
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
with (
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=inbound)),
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(
side_effect=lambda _db, warehouse_id, **_kwargs: SimpleNamespace(
warehouse_id=warehouse_id,
status='0',
)
),
),
patch.object(
InboundService,
'_lock_and_validate_pallets',
new=AsyncMock(return_value=([make_inventory(2)], [make_pallet(2)])),
),
patch.object(OperationDao, 'update_inbound', new=update),
patch.object(OperationDao, 'replace_inbound_items', new=replace),
self.assertRaises(PermissionException) as raised,
):
await InboundService.edit_inbound_services(self.db, page, self.user)
self.assertEqual('无权访问该入库单', raised.exception.message)
warehouse_mapping.assert_any_await(self.db, self.user, 7, for_update=True)
warehouse_mapping.assert_any_await(self.db, self.user, 8, for_update=True)
update.assert_not_awaited()
replace.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_edit_rejects_confirmed_document_without_pallet_or_order_write(self) -> None:
page = InboundUpdateModel(inboundId=11, warehouseId=8, palletIds=[2])
update = AsyncMock()
@@ -484,6 +585,16 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
access = AsyncMock()
with (
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(side_effect=lock)),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(
side_effect=lambda _db, warehouse_id, **_kwargs: SimpleNamespace(
warehouse_id=warehouse_id,
status='0',
)
),
),
patch.object(AccessService, 'assert_warehouse_access', new=access),
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=delete)),
):
@@ -494,10 +605,58 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
actions,
)
self.default_scope.assert_awaited_once_with(self.db, 7)
access.assert_not_awaited()
self.assertEqual([2, 5], [call.args[2] for call in access.await_args_list])
self.assertTrue(all(call.kwargs.get('for_update') for call in access.await_args_list))
self.assertTrue(result.is_success)
self.db.rollback.assert_not_awaited()
async def test_batch_delete_rechecks_all_current_warehouses_before_first_delete(self) -> None:
revoked_warehouse_id = 8
documents = {
11: make_inbound(inbound_id=11, warehouse_id=revoked_warehouse_id),
12: make_inbound(inbound_id=12, warehouse_id=7),
}
async def warehouse_access(
_db: AsyncSession,
_user: CurrentUserModel,
warehouse_id: int,
**kwargs: Any,
) -> None:
if warehouse_id == revoked_warehouse_id and kwargs.get('for_update'):
raise PermissionException(data='', message='无权访问该仓库')
delete = AsyncMock()
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
with (
patch.object(
OperationDao,
'get_inbound_for_update',
new=AsyncMock(side_effect=lambda _db, inbound_id, **_kwargs: documents[inbound_id]),
),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(
side_effect=lambda _db, warehouse_id, **_kwargs: SimpleNamespace(
warehouse_id=warehouse_id,
status='0',
)
),
),
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
patch.object(OperationDao, 'delete_inbound', new=delete),
self.assertRaises(PermissionException) as raised,
):
await InboundService.delete_inbound_services(self.db, [12, 11], self.user)
self.assertEqual('无权访问该入库单', raised.exception.message)
self.assertEqual([7, 8], [call.args[2] for call in warehouse_mapping.await_args_list])
self.assertTrue(all(call.kwargs.get('for_update') for call in warehouse_mapping.await_args_list))
delete.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_batch_delete_is_atomic_when_later_document_is_confirmed(self) -> None:
delete = AsyncMock()
records = {
@@ -583,6 +742,11 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
'get_inbound_for_update',
new=AsyncMock(return_value=make_inbound(pallet_ids=(2,))),
),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(ServiceException),
@@ -924,10 +1088,10 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
actions.append('detail')
return inbound
async def access(*_args: Any) -> None:
async def access(*_args: Any, **_kwargs: Any) -> None:
actions.append('access')
async def by_key(*_args: Any) -> None:
async def by_key(*_args: Any, **_kwargs: Any) -> None:
actions.append('idempotency-read')
async def lock_order(*_args: Any, **_kwargs: Any) -> BizInboundOrder:
@@ -942,6 +1106,10 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
actions.append('pallet-lock')
return pallets
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
actions.append('warehouse-lock')
return SimpleNamespace(warehouse_id=7, status='0')
def transition(current: PalletFlowState, target: PalletFlowState) -> None:
actions.append(f'transition-{current.value}-{target.value}')
@@ -985,11 +1153,12 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'update_inbound', new=AsyncMock(side_effect=update)),
patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=add_event)),
patch.object(inbound_service_module, 'datetime'),
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(side_effect=lock_warehouse)),
)
with (
patches[0] as detail_read,
patches[1] as access_probe,
patches[2],
patches[2] as key_read,
patches[3] as order_lock,
patches[4] as inventory_lock,
patches[5] as pallet_lock,
@@ -997,6 +1166,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patches[7],
patches[8],
patches[9] as service_datetime,
patches[10] as warehouse_lock,
):
service_datetime.now.return_value = self.fixed_time
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
@@ -1008,10 +1178,12 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
'events': events,
'actions': actions,
'detail_read': detail_read,
'key_read': key_read,
'access_probe': access_probe,
'order_lock': order_lock,
'inventory_lock': inventory_lock,
'pallet_lock': pallet_lock,
'warehouse_lock': warehouse_lock,
'ensure_transition': ensure_transition,
}
@@ -1025,6 +1197,8 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
'order-lock',
'inventory-lock',
'pallet-lock',
'warehouse-lock',
'access',
'transition-UNSTOCKED-EMPTY_IN_WAREHOUSE',
'transition-UNSTOCKED-EMPTY_IN_WAREHOUSE',
'update-order',
@@ -1036,8 +1210,14 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
)
self.default_scope.assert_awaited_once_with(self.db, 7)
captured['detail_read'].assert_awaited_once_with(self.db, 11, warehouse_ids=[7])
captured['key_read'].assert_awaited_once_with(
self.db,
'confirm-key',
warehouse_ids=[7],
)
captured['order_lock'].assert_awaited_once_with(self.db, 11, warehouse_ids=[7])
captured['access_probe'].assert_not_awaited()
captured['warehouse_lock'].assert_awaited_once_with(self.db, 7, for_update=True)
captured['access_probe'].assert_awaited_once_with(self.db, self.user, 7, for_update=True)
captured['inventory_lock'].assert_awaited_once_with(self.db, [2, 5])
captured['pallet_lock'].assert_awaited_once_with(self.db, [2, 5])
captured['ensure_transition'].assert_has_calls(
@@ -1085,6 +1265,52 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
self.db.rollback.assert_not_awaited()
self.assertTrue(captured['result'].is_success)
async def test_confirm_hides_document_when_mapping_is_revoked_after_pallet_locks(self) -> None:
inbound = make_inbound(pallet_ids=(2,))
inventory = make_inventory(2)
async def warehouse_access(*_args: Any, **kwargs: Any) -> None:
if kwargs.get('for_update'):
raise PermissionException(data='', message='无权访问该仓库')
update = AsyncMock()
add_event = AsyncMock()
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
with (
patch.object(OperationDao, 'get_inbound_by_id', new=AsyncMock(return_value=inbound)),
patch.object(
OperationDao,
'get_inbound_by_confirm_idempotency_key',
new=AsyncMock(return_value=None),
),
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=inbound)),
patch.object(
InboundService,
'_lock_and_validate_pallets',
new=AsyncMock(return_value=([inventory], [make_pallet(2)])),
),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
) as warehouse_lock,
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
patch.object(OperationDao, 'update_inbound', new=update),
patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised,
):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
self.assertEqual('无权访问该入库单', raised.exception.message)
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True)
warehouse_mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True)
self.assertEqual('DRAFT', inbound.status)
self.assertEqual('UNSTOCKED', inventory.flow_state)
update.assert_not_awaited()
add_event.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_confirm_rejects_incomplete_locked_sets_before_any_mutation(self) -> None:
inbound = make_inbound()
inventories = [make_inventory(2)]
@@ -1124,6 +1350,11 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=inbound)),
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=[inventory])),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[make_pallet(2)])),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
),
patch.object(PalletStateMachine, 'ensure_transition', side_effect=ValueError('invalid')),
patch.object(OperationDao, 'update_inbound', new=update),
patch.object(InventoryDao, 'add_event', new=add_event),
@@ -1154,16 +1385,67 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
),
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=confirmed)) as lock,
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
) as warehouse_lock,
):
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
lock.assert_awaited_once_with(self.db, 11, warehouse_ids=[7])
access.assert_awaited_once_with(self.db, self.user, 7)
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True)
self.assertEqual(
[
unittest.mock.call(self.db, self.user, 7),
unittest.mock.call(self.db, self.user, 7, for_update=True),
],
access.await_args_list,
)
inventory_lock.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
self.assertTrue(result.is_success)
async def test_same_key_retry_hides_document_when_current_mapping_is_revoked(self) -> None:
confirmed = make_inbound(
status='CONFIRMED',
idempotency_key='confirm-key',
request_hash=self.request_hash,
)
async def warehouse_access(*_args: Any, **kwargs: Any) -> None:
if kwargs.get('for_update'):
raise PermissionException(data='', message='无权访问该仓库')
inventory_lock = AsyncMock()
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
with (
patch.object(OperationDao, 'get_inbound_by_id', new=AsyncMock(return_value=confirmed)),
patch.object(
OperationDao,
'get_inbound_by_confirm_idempotency_key',
new=AsyncMock(return_value=confirmed),
),
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=confirmed)),
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='1')),
) as warehouse_lock,
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
self.assertRaises(PermissionException) as raised,
):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
self.assertEqual('无权访问该入库单', raised.exception.message)
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True)
warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True)
inventory_lock.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_same_key_for_different_inbound_is_rejected_without_lock_or_write(self) -> None:
target = make_inbound()
existing = make_inbound(
@@ -1225,6 +1507,11 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=locked)),
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[make_pallet(5)])),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
),
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
):
@@ -1246,23 +1533,96 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
)
key_reads = AsyncMock(side_effect=[None, recovered])
self.db.commit.side_effect = integrity_error()
parent_lock = AsyncMock(side_effect=[inbound, recovered])
with (
patch.object(OperationDao, 'get_inbound_by_id', new=AsyncMock(return_value=inbound)),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()) as access,
patch.object(OperationDao, 'get_inbound_by_confirm_idempotency_key', new=key_reads),
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=inbound)),
patch.object(OperationDao, 'get_inbound_for_update', new=parent_lock),
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=[inventory])),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[make_pallet(2)])),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
),
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
):
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
access.assert_awaited_once_with(self.db, self.user, 7)
self.assertEqual(2, parent_lock.await_count)
self.assertEqual(
[
unittest.mock.call(self.db, self.user, 7, for_update=True),
unittest.mock.call(self.db, self.user, 7, for_update=True),
],
access.await_args_list,
)
self.assertEqual(2, key_reads.await_count)
key_reads.assert_has_awaits(
[
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]),
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]),
]
)
self.assertEqual(2, self.db.rollback.await_count)
self.assertTrue(result.is_success)
async def test_integrity_recovery_locks_parent_and_rechecks_revoked_current_mapping(self) -> None:
inbound = make_inbound(pallet_ids=(2,))
recovered = make_inbound(
pallet_ids=(2,),
status='CONFIRMED',
idempotency_key='confirm-key',
request_hash=self.request_hash,
)
locked_access_count = 0
recovery_access_number = 2
async def warehouse_access(*_args: Any, **kwargs: Any) -> None:
nonlocal locked_access_count
if not kwargs.get('for_update'):
return
locked_access_count += 1
if locked_access_count == recovery_access_number:
raise PermissionException(data='', message='无权访问该仓库')
self.db.commit.side_effect = integrity_error()
parent_lock = AsyncMock(side_effect=[inbound, recovered])
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
with (
patch.object(OperationDao, 'get_inbound_by_id', new=AsyncMock(return_value=inbound)),
patch.object(
OperationDao,
'get_inbound_by_confirm_idempotency_key',
new=AsyncMock(side_effect=[None, recovered]),
),
patch.object(OperationDao, 'get_inbound_for_update', new=parent_lock),
patch.object(
InboundService,
'_lock_and_validate_pallets',
new=AsyncMock(return_value=([make_inventory(2)], [make_pallet(2)])),
),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
) as warehouse_lock,
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
self.assertRaises(PermissionException) as raised,
):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user)
self.assertEqual('无权访问该入库单', raised.exception.message)
self.assertEqual(2, parent_lock.await_count)
parent_lock.assert_awaited_with(self.db, 11, warehouse_ids=[7])
self.assertEqual(2, warehouse_lock.await_count)
self.assertEqual(2, locked_access_count)
self.assertEqual(2, self.db.rollback.await_count)
async def test_integrity_conflict_with_mismatched_recovery_is_stable_failure(self) -> None:
inbound = make_inbound(pallet_ids=(2,))
inventory = make_inventory(2)
@@ -1284,6 +1644,11 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(return_value=inbound)),
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=[inventory])),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[make_pallet(2)])),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
),
patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
patch.object(InventoryDao, 'add_event', new=AsyncMock()),
self.assertRaises(ServiceException),