功能:实现多托盘发货出库
This commit is contained in:
@@ -122,11 +122,16 @@ class TestDaoPublicContract(unittest.TestCase):
|
||||
def test_operation_dao_critical_parameter_names_and_defaults_are_stable(self) -> None:
|
||||
expected = {
|
||||
'get_inbound_for_update': ('db', 'inbound_id', 'warehouse_ids'),
|
||||
'get_shipment_for_update': ('db', 'shipment_id'),
|
||||
'get_shipment_for_update': ('db', 'shipment_id', 'warehouse_ids', 'site_ids'),
|
||||
'get_return_for_update': ('db', 'return_id'),
|
||||
'get_active_filling_for_update': ('db', 'filling_id'),
|
||||
'get_inbound_by_confirm_idempotency_key': ('db', 'idempotency_key'),
|
||||
'get_shipment_by_confirm_idempotency_key': ('db', 'idempotency_key'),
|
||||
'get_shipment_by_confirm_idempotency_key': (
|
||||
'db',
|
||||
'idempotency_key',
|
||||
'warehouse_ids',
|
||||
'site_ids',
|
||||
),
|
||||
'get_filling_by_idempotency_key': ('db', 'idempotency_key', 'warehouse_ids'),
|
||||
'get_usage_by_idempotency_key': ('db', 'idempotency_key'),
|
||||
'get_return_by_request_idempotency_key': ('db', 'idempotency_key'),
|
||||
@@ -139,7 +144,7 @@ class TestDaoPublicContract(unittest.TestCase):
|
||||
'get_filling_by_id': ('db', 'filling_id', 'warehouse_ids'),
|
||||
'add_filling_record': ('db', 'filling'),
|
||||
'update_filling_record': ('db', 'filling'),
|
||||
'get_shipment_by_id': ('db', 'shipment_id'),
|
||||
'get_shipment_by_id': ('db', 'shipment_id', 'warehouse_ids', 'site_ids'),
|
||||
'add_shipment': ('db', 'shipment'),
|
||||
'update_shipment': ('db', 'shipment'),
|
||||
'delete_shipment': ('db', 'shipment_id'),
|
||||
@@ -177,6 +182,16 @@ class TestDaoPublicContract(unittest.TestCase):
|
||||
if warehouse_ids is not None:
|
||||
self.assertIsNone(warehouse_ids.default)
|
||||
|
||||
for method_name in (
|
||||
'get_shipment_for_update',
|
||||
'get_shipment_by_confirm_idempotency_key',
|
||||
'get_shipment_by_id',
|
||||
):
|
||||
with self.subTest(method=method_name):
|
||||
signature = inspect.signature(getattr(OperationDao, method_name))
|
||||
self.assertIsNone(signature.parameters['warehouse_ids'].default)
|
||||
self.assertIsNone(signature.parameters['site_ids'].default)
|
||||
|
||||
filling_list_signature = inspect.signature(OperationDao.get_filling_list)
|
||||
self.assertEqual(
|
||||
('db', 'query_object', 'is_page', 'warehouse_ids'),
|
||||
@@ -185,7 +200,16 @@ class TestDaoPublicContract(unittest.TestCase):
|
||||
self.assertIs(True, filling_list_signature.parameters['is_page'].default)
|
||||
self.assertIsNone(filling_list_signature.parameters['warehouse_ids'].default)
|
||||
|
||||
for method_name in ('get_shipment_list', 'get_usage_list', 'get_return_list'):
|
||||
shipment_list_signature = inspect.signature(OperationDao.get_shipment_list)
|
||||
self.assertEqual(
|
||||
('db', 'query_object', 'is_page', 'warehouse_ids', 'site_ids'),
|
||||
tuple(shipment_list_signature.parameters),
|
||||
)
|
||||
self.assertIs(True, shipment_list_signature.parameters['is_page'].default)
|
||||
self.assertIsNone(shipment_list_signature.parameters['warehouse_ids'].default)
|
||||
self.assertIsNone(shipment_list_signature.parameters['site_ids'].default)
|
||||
|
||||
for method_name in ('get_usage_list', 'get_return_list'):
|
||||
with self.subTest(method=method_name):
|
||||
signature = inspect.signature(getattr(OperationDao, method_name))
|
||||
self.assertEqual(('db', 'query_object', 'is_page'), tuple(signature.parameters))
|
||||
@@ -383,13 +407,6 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_for_update_queries_target_primary_keys_and_lock_rows(self) -> None:
|
||||
cases = (
|
||||
(
|
||||
OperationDao.get_shipment_for_update,
|
||||
BizShipmentOrder,
|
||||
'shipment_id',
|
||||
12,
|
||||
(),
|
||||
),
|
||||
(
|
||||
OperationDao.get_return_for_update,
|
||||
BizReturnOrder,
|
||||
@@ -426,6 +443,69 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
|
||||
for fragment in extra_fragments:
|
||||
self.assertIn(fragment, sql)
|
||||
|
||||
async def test_shipment_for_update_locks_scoped_parent_then_current_items(self) -> None:
|
||||
shipment = SimpleNamespace(shipment_id=12)
|
||||
items = [
|
||||
SimpleNamespace(shipment_item_id=1, shipment_id=12, pallet_id=5),
|
||||
SimpleNamespace(shipment_item_id=3, shipment_id=12, pallet_id=2),
|
||||
]
|
||||
parent_result = MagicMock()
|
||||
parent_result.scalar_one_or_none.return_value = shipment
|
||||
item_result = MagicMock()
|
||||
item_result.scalars.return_value.all.return_value = items
|
||||
self.db.execute.side_effect = [parent_result, item_result]
|
||||
|
||||
actual = await OperationDao.get_shipment_for_update(
|
||||
self.db,
|
||||
12,
|
||||
warehouse_ids=[7, 3, 7],
|
||||
site_ids=[11, 9, 11],
|
||||
)
|
||||
|
||||
self.assertIs(shipment, actual)
|
||||
self.assertEqual([2, 5], [item.pallet_id for item in actual.items])
|
||||
self.assertEqual(2, self.db.execute.await_count)
|
||||
parent_statement = self.db.execute.await_args_list[0].args[0]
|
||||
item_statement = self.db.execute.await_args_list[1].args[0]
|
||||
self.assertTrue(parent_statement.get_final_froms()[0].compare(BizShipmentOrder.__table__))
|
||||
self.assertTrue(item_statement.get_final_froms()[0].compare(BizShipmentItem.__table__))
|
||||
self.assertIs(True, parent_statement.get_execution_options().get('populate_existing'))
|
||||
self.assertIs(True, item_statement.get_execution_options().get('populate_existing'))
|
||||
for dialect in (mysql.dialect(), postgresql.dialect()):
|
||||
with self.subTest(dialect=dialect.name):
|
||||
parent_sql = compile_sql(parent_statement, dialect)
|
||||
item_sql = compile_sql(item_statement, dialect)
|
||||
self.assertIn('biz_shipment_order.shipment_id = 12', parent_sql)
|
||||
self.assertIn('biz_shipment_order.source_warehouse_id IN (3, 7)', parent_sql)
|
||||
self.assertIn('biz_shipment_order.target_site_id IN (9, 11)', parent_sql)
|
||||
self.assertIn('FOR UPDATE', parent_sql)
|
||||
self.assertIn('biz_shipment_item.shipment_id = 12', item_sql)
|
||||
self.assertIn(
|
||||
'ORDER BY biz_shipment_item.pallet_id, biz_shipment_item.shipment_item_id',
|
||||
item_sql,
|
||||
)
|
||||
self.assertIn('FOR UPDATE', item_sql)
|
||||
|
||||
async def test_shipment_for_update_missing_scoped_parent_skips_item_lock(self) -> None:
|
||||
parent_result = MagicMock()
|
||||
parent_result.scalar_one_or_none.return_value = None
|
||||
self.db.execute.return_value = parent_result
|
||||
|
||||
actual = await OperationDao.get_shipment_for_update(
|
||||
self.db,
|
||||
12,
|
||||
warehouse_ids=[],
|
||||
site_ids=[9],
|
||||
)
|
||||
|
||||
self.assertIsNone(actual)
|
||||
self.db.execute.assert_awaited_once()
|
||||
parent_statement = self.db.execute.await_args.args[0]
|
||||
mysql_sql = compile_sql(parent_statement, mysql.dialect())
|
||||
postgresql_sql = compile_sql(parent_statement, postgresql.dialect())
|
||||
self.assertIn('false = 1', mysql_sql)
|
||||
self.assertIn('false', postgresql_sql)
|
||||
|
||||
async def test_inbound_for_update_locks_parent_then_current_items(self) -> None:
|
||||
inbound = SimpleNamespace(inbound_id=11)
|
||||
items = [
|
||||
@@ -716,6 +796,74 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertNotIn('warehouse_id IN', sql)
|
||||
self.assertNotIn('WHERE false', sql)
|
||||
|
||||
async def test_shipment_list_applies_both_scopes_and_empty_scope_is_false(self) -> None:
|
||||
query = ShipmentPageQueryModel(page_num=2, page_size=7)
|
||||
marker = object()
|
||||
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as paginate:
|
||||
actual = await OperationDao.get_shipment_list(
|
||||
self.db,
|
||||
query,
|
||||
is_page=True,
|
||||
warehouse_ids=[7, 3, 7],
|
||||
site_ids=[11, 9, 11],
|
||||
)
|
||||
|
||||
self.assertIs(marker, actual)
|
||||
statement = paginate.await_args.args[1]
|
||||
for dialect in (mysql.dialect(), postgresql.dialect()):
|
||||
with self.subTest(dialect=dialect.name):
|
||||
sql = compile_sql(statement, dialect)
|
||||
self.assertIn('biz_shipment_order.source_warehouse_id IN (3, 7)', sql)
|
||||
self.assertIn('biz_shipment_order.target_site_id IN (9, 11)', sql)
|
||||
|
||||
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as empty_paginate:
|
||||
await OperationDao.get_shipment_list(
|
||||
self.db,
|
||||
query,
|
||||
warehouse_ids=[7],
|
||||
site_ids=[],
|
||||
)
|
||||
empty_statement = empty_paginate.await_args.args[1]
|
||||
self.assertIn('false = 1', compile_sql(empty_statement, mysql.dialect()))
|
||||
self.assertIn('false', compile_sql(empty_statement, postgresql.dialect()))
|
||||
|
||||
async def test_shipment_detail_and_confirm_key_push_both_scopes_into_parent_sql(self) -> None:
|
||||
parent_result = MagicMock()
|
||||
parent_result.scalar_one_or_none.return_value = None
|
||||
self.db.execute.return_value = parent_result
|
||||
|
||||
self.assertIsNone(
|
||||
await OperationDao.get_shipment_by_id(
|
||||
self.db,
|
||||
12,
|
||||
warehouse_ids=[7, 3],
|
||||
site_ids=[11, 9],
|
||||
)
|
||||
)
|
||||
self.db.execute.assert_awaited_once()
|
||||
detail_statement = self.db.execute.await_args.args[0]
|
||||
|
||||
self.db.reset_mock()
|
||||
self.db.execute.return_value = parent_result
|
||||
self.assertIsNone(
|
||||
await OperationDao.get_shipment_by_confirm_idempotency_key(
|
||||
self.db,
|
||||
'confirm-key',
|
||||
warehouse_ids=[7, 3],
|
||||
site_ids=[11, 9],
|
||||
)
|
||||
)
|
||||
key_statement = self.db.execute.await_args.args[0]
|
||||
for dialect in (mysql.dialect(), postgresql.dialect()):
|
||||
with self.subTest(dialect=dialect.name):
|
||||
detail_sql = compile_sql(detail_statement, dialect)
|
||||
key_sql = compile_sql(key_statement, dialect)
|
||||
for sql in (detail_sql, key_sql):
|
||||
self.assertIn('biz_shipment_order.source_warehouse_id IN (3, 7)', sql)
|
||||
self.assertIn('biz_shipment_order.target_site_id IN (9, 11)', sql)
|
||||
self.assertIn("biz_shipment_order.confirm_idempotency_key = 'confirm-key'", key_sql)
|
||||
self.assertNotIn('FOR UPDATE', key_sql)
|
||||
|
||||
def test_time_conditions_include_fractional_last_second_with_exclusive_next_day(self) -> None:
|
||||
lower, upper = OperationDao._time_conditions(
|
||||
BizUsageRecord.occurred_at,
|
||||
|
||||
Reference in New Issue
Block a user