修复:补充发货草稿当前余量

This commit is contained in:
焦龙言
2026-07-27 09:05:04 +08:00
parent d5da207206
commit e4477f321c
5 changed files with 83 additions and 12 deletions
@@ -11,6 +11,7 @@ from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
BizInboundItem, BizInboundItem,
BizInboundOrder, BizInboundOrder,
BizPalletInventory,
BizReturnItem, BizReturnItem,
BizReturnOrder, BizReturnOrder,
BizShipmentItem, BizShipmentItem,
@@ -645,16 +646,31 @@ class OperationDao:
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None, site_ids: list[int] | None = None,
) -> BizShipmentOrder | None: ) -> BizShipmentOrder | None:
return await cls._get_order_detail( shipment = await cls._get_by_id(
db, db,
BizShipmentOrder, BizShipmentOrder,
BizShipmentOrder.shipment_id, BizShipmentOrder.shipment_id,
shipment_id, shipment_id,
BizShipmentItem,
BizShipmentItem.shipment_id,
BizShipmentItem.shipment_item_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids), *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 @classmethod
async def add_shipment( async def add_shipment(
@@ -269,6 +269,12 @@ class ShipmentItemModel(BaseModel):
pallet_id: int = Field(gt=0, description='托盘ID') pallet_id: int = Field(gt=0, description='托盘ID')
active_filling_id: int | None = Field(default=None, 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='确认时包数快照') 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_by: str | None = Field(default=None, max_length=64, description='创建者')
create_time: datetime | None = Field(default=None, description='创建时间') create_time: datetime | None = Field(default=None, description='创建时间')
@@ -1018,14 +1018,6 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
'inbound_item_id', 'inbound_item_id',
11, 11,
), ),
(
OperationDao.get_shipment_by_id,
BizShipmentOrder,
'shipment_id',
BizShipmentItem,
'shipment_item_id',
12,
),
( (
OperationDao.get_return_by_id, OperationDao.get_return_by_id,
BizReturnOrder, BizReturnOrder,
@@ -10,6 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from sqlalchemy.dialects import postgresql
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -251,6 +252,49 @@ class ShipmentInfrastructureContractTest(unittest.TestCase):
self.assertIs(False, parameter.default) 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): class ShipmentControllerBootstrapTest(unittest.TestCase):
def test_shipment_controller_module_exists(self) -> None: def test_shipment_controller_module_exists(self) -> None:
spec = importlib.util.find_spec('module_business.controller.shipment_controller') spec = importlib.util.find_spec('module_business.controller.shipment_controller')
@@ -229,6 +229,7 @@ OPERATION_MODEL_FIELDS = {
'pallet_id', 'pallet_id',
'active_filling_id', 'active_filling_id',
'package_qty', 'package_qty',
'remaining_packages',
'create_by', 'create_by',
'create_time', 'create_time',
}, },
@@ -611,6 +612,7 @@ STRICT_POSITIVE_PACKAGE_FIELDS = {
} }
STRICT_NON_NEGATIVE_PACKAGE_FIELDS = { STRICT_NON_NEGATIVE_PACKAGE_FIELDS = {
ShipmentItemModel: {'remaining_packages'},
UsageModel: {'remaining_packages_after'}, UsageModel: {'remaining_packages_after'},
ReturnItemModel: {'remaining_packages_snapshot'}, ReturnItemModel: {'remaining_packages_snapshot'},
PalletScanResponseModel: {'remaining_packages'}, PalletScanResponseModel: {'remaining_packages'},
@@ -649,6 +651,7 @@ PACKAGE_MODEL_PAYLOADS = {
'shipment_id': 1, 'shipment_id': 1,
'pallet_id': 1, 'pallet_id': 1,
'package_qty': 20, 'package_qty': 20,
'remaining_packages': 8,
}, },
UsageModel: { UsageModel: {
'usage_id': 1, 'usage_id': 1,
@@ -939,6 +942,16 @@ class BusinessVoContractTest(unittest.TestCase):
accepted_invalid_values.append((model_class.__name__, field_name, repr(invalid_value))) accepted_invalid_values.append((model_class.__name__, field_name, repr(invalid_value)))
self.assertEqual([], accepted_invalid_values) 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: def test_id_lists_reject_empty_non_positive_and_duplicate_values(self) -> None:
cases = ( cases = (