修复:补充发货草稿当前余量
This commit is contained in:
@@ -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 = (
|
||||||
|
|||||||
Reference in New Issue
Block a user