From e4477f321ccf168359fcb20ff64cb7420a4645f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Mon, 27 Jul 2026 09:05:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=8F=91=E8=B4=A7=E8=8D=89=E7=A8=BF=E5=BD=93=E5=89=8D=E4=BD=99?= =?UTF-8?q?=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_business/dao/operation_dao.py | 24 ++++++++-- .../module_business/entity/vo/operation_vo.py | 6 +++ .../module_business/test_inventory_dao.py | 8 ---- .../module_business/test_shipment_service.py | 44 +++++++++++++++++++ .../tests/module_business/test_vo_contract.py | 13 ++++++ 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/shierfeng-fastapi-backend/module_business/dao/operation_dao.py b/shierfeng-fastapi-backend/module_business/dao/operation_dao.py index d3414fa..85fea8a 100644 --- a/shierfeng-fastapi-backend/module_business/dao/operation_dao.py +++ b/shierfeng-fastapi-backend/module_business/dao/operation_dao.py @@ -11,6 +11,7 @@ from module_business.entity.do.operation_do import ( BizFillingRecord, BizInboundItem, BizInboundOrder, + BizPalletInventory, BizReturnItem, BizReturnOrder, BizShipmentItem, @@ -645,16 +646,31 @@ class OperationDao: warehouse_ids: list[int] | None = None, site_ids: list[int] | None = None, ) -> BizShipmentOrder | None: - return await cls._get_order_detail( + shipment = await cls._get_by_id( db, BizShipmentOrder, BizShipmentOrder.shipment_id, shipment_id, - BizShipmentItem, - BizShipmentItem.shipment_id, - BizShipmentItem.shipment_item_id, *cls._shipment_scope_conditions(warehouse_ids, site_ids), ) + if shipment is None: + return None + + item_result = await db.execute( + select(BizShipmentItem, BizPalletInventory.remaining_packages) + .outerjoin( + BizPalletInventory, + BizPalletInventory.pallet_id == BizShipmentItem.pallet_id, + ) + .where(BizShipmentItem.shipment_id == shipment_id) + .order_by(BizShipmentItem.shipment_item_id) + ) + items = [] + for item, remaining_packages in item_result.all(): + item.remaining_packages = remaining_packages + items.append(item) + shipment.items = items + return shipment @classmethod async def add_shipment( diff --git a/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py b/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py index ef8dc4e..0ce5b29 100644 --- a/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py +++ b/shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py @@ -269,6 +269,12 @@ class ShipmentItemModel(BaseModel): pallet_id: int = Field(gt=0, description='托盘ID') active_filling_id: int | None = Field(default=None, gt=0, description='确认时活动装填记录ID') package_qty: int | None = Field(default=None, strict=True, gt=0, description='确认时包数快照') + remaining_packages: int | None = Field( + default=None, + strict=True, + ge=0, + description='当前库存剩余包数(只读,非确认快照)', + ) create_by: str | None = Field(default=None, max_length=64, description='创建者') create_time: datetime | None = Field(default=None, description='创建时间') diff --git a/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py b/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py index 744d35c..d245d30 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py @@ -1018,14 +1018,6 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase): 'inbound_item_id', 11, ), - ( - OperationDao.get_shipment_by_id, - BizShipmentOrder, - 'shipment_id', - BizShipmentItem, - 'shipment_item_id', - 12, - ), ( OperationDao.get_return_by_id, BizReturnOrder, diff --git a/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py b/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py index 183a8bd..ba805e6 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py @@ -10,6 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from fastapi import FastAPI from fastapi.routing import APIRoute +from sqlalchemy.dialects import postgresql from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession @@ -251,6 +252,49 @@ class ShipmentInfrastructureContractTest(unittest.TestCase): self.assertIs(False, parameter.default) +class ShipmentDetailDaoTest(unittest.IsolatedAsyncioTestCase): + async def test_detail_loads_each_current_remaining_quantity_with_one_left_join(self) -> None: + db = MagicMock(spec=AsyncSession) + shipment = make_shipment(pallet_ids=(5, 2, 8)) + order_result = MagicMock() + order_result.scalar_one_or_none.return_value = shipment + item_result = MagicMock() + item_result.all.return_value = [ + (shipment.items[0], 13), + (shipment.items[1], 6), + (shipment.items[2], None), + ] + db.execute.side_effect = [order_result, item_result] + + actual = await OperationDao.get_shipment_by_id( + db, + 12, + warehouse_ids=[7], + site_ids=[9], + ) + + self.assertIs(shipment, actual) + self.assertEqual(2, db.execute.await_count) + self.assertEqual([13, 6, None], [item.remaining_packages for item in shipment.items]) + self.assertEqual([None, None, None], [item.package_qty for item in shipment.items]) + item_statement = db.execute.await_args_list[1].args[0] + item_sql = ' '.join( + str( + item_statement.compile( + dialect=postgresql.dialect(), + compile_kwargs={'literal_binds': True}, + ) + ).split() + ) + self.assertIn( + 'LEFT OUTER JOIN biz_pallet_inventory ' + 'ON biz_pallet_inventory.pallet_id = biz_shipment_item.pallet_id', + item_sql, + ) + self.assertIn('WHERE biz_shipment_item.shipment_id = 12', item_sql) + self.assertIn('ORDER BY biz_shipment_item.shipment_item_id', item_sql) + + class ShipmentControllerBootstrapTest(unittest.TestCase): def test_shipment_controller_module_exists(self) -> None: spec = importlib.util.find_spec('module_business.controller.shipment_controller') diff --git a/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py b/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py index 88d4e06..7371eca 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py @@ -229,6 +229,7 @@ OPERATION_MODEL_FIELDS = { 'pallet_id', 'active_filling_id', 'package_qty', + 'remaining_packages', 'create_by', 'create_time', }, @@ -611,6 +612,7 @@ STRICT_POSITIVE_PACKAGE_FIELDS = { } STRICT_NON_NEGATIVE_PACKAGE_FIELDS = { + ShipmentItemModel: {'remaining_packages'}, UsageModel: {'remaining_packages_after'}, ReturnItemModel: {'remaining_packages_snapshot'}, PalletScanResponseModel: {'remaining_packages'}, @@ -649,6 +651,7 @@ PACKAGE_MODEL_PAYLOADS = { 'shipment_id': 1, 'pallet_id': 1, 'package_qty': 20, + 'remaining_packages': 8, }, UsageModel: { 'usage_id': 1, @@ -939,6 +942,16 @@ class BusinessVoContractTest(unittest.TestCase): accepted_invalid_values.append((model_class.__name__, field_name, repr(invalid_value))) self.assertEqual([], accepted_invalid_values) + shipment_item = ShipmentItemModel.model_validate( + PACKAGE_MODEL_PAYLOADS[ShipmentItemModel] + | { + 'packageQty': None, + 'remainingPackages': None, + } + ) + self.assertIsNone(shipment_item.package_qty) + self.assertIsNone(shipment_item.remaining_packages) + self.assertIn('remainingPackages', shipment_item.model_dump(by_alias=True)) def test_id_lists_reject_empty_non_positive_and_duplicate_values(self) -> None: cases = (