修复:收紧托盘业务包数校验
This commit is contained in:
@@ -589,6 +589,163 @@ PAGE_QUERY_MODELS = (
|
||||
PalletTraceReportQueryModel,
|
||||
)
|
||||
|
||||
STRICT_POSITIVE_PACKAGE_FIELDS = {
|
||||
PalletModel: {'capacity_packages'},
|
||||
FillingModel: {'initial_packages'},
|
||||
ShipmentItemModel: {'package_qty'},
|
||||
UsageModel: {'used_packages'},
|
||||
PalletScanResponseModel: {'capacity_packages'},
|
||||
PalletInventoryReportModel: {'initial_packages'},
|
||||
FillingReportModel: {'initial_packages'},
|
||||
ShipmentReportModel: {'package_qty'},
|
||||
ConsumptionReportModel: {'used_packages'},
|
||||
}
|
||||
|
||||
STRICT_NON_NEGATIVE_PACKAGE_FIELDS = {
|
||||
UsageModel: {'remaining_packages_after'},
|
||||
ReturnItemModel: {'remaining_packages_snapshot'},
|
||||
PalletScanResponseModel: {'remaining_packages'},
|
||||
PalletInventoryReportModel: {'remaining_packages'},
|
||||
ConsumptionReportModel: {'remaining_packages_after'},
|
||||
ReturnReportModel: {'remaining_packages_snapshot'},
|
||||
PalletTraceReportModel: {'before_remaining_packages', 'after_remaining_packages'},
|
||||
}
|
||||
|
||||
PACKAGE_MODEL_PAYLOADS = {
|
||||
PalletModel: {
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'qr_token': 'qr-12345678',
|
||||
'capacity_packages': 20,
|
||||
'asset_status': '0',
|
||||
},
|
||||
FillingModel: {
|
||||
'filling_id': 1,
|
||||
'filling_no': 'F202607240001',
|
||||
'pallet_id': 1,
|
||||
'warehouse_id': 1,
|
||||
'strain_id': 1,
|
||||
'strain_code_snapshot': 'STRAIN-01',
|
||||
'strain_name_snapshot': '菌种一号',
|
||||
'batch_no': 'B20260724',
|
||||
'package_spec': '20kg/包',
|
||||
'initial_packages': 20,
|
||||
'idempotency_key': 'fill-key-1',
|
||||
'request_hash': 'fill-hash-1',
|
||||
'operator_id': 1,
|
||||
'operated_at': '2026-07-24T10:00:00',
|
||||
},
|
||||
ShipmentItemModel: {
|
||||
'shipment_item_id': 1,
|
||||
'shipment_id': 1,
|
||||
'pallet_id': 1,
|
||||
'package_qty': 20,
|
||||
},
|
||||
UsageModel: {
|
||||
'usage_id': 1,
|
||||
'usage_no': 'U202607240001',
|
||||
'pallet_id': 1,
|
||||
'active_filling_id': 1,
|
||||
'site_id': 1,
|
||||
'line_id': 1,
|
||||
'used_packages': 1,
|
||||
'remaining_packages_after': 19,
|
||||
'idempotency_key': 'usage-key-1',
|
||||
'request_hash': 'usage-hash-1',
|
||||
'operator_id': 1,
|
||||
'occurred_at': '2026-07-24T11:00:00',
|
||||
},
|
||||
ReturnItemModel: {
|
||||
'return_item_id': 1,
|
||||
'return_id': 1,
|
||||
'pallet_id': 1,
|
||||
'remaining_packages_snapshot': 0,
|
||||
},
|
||||
PalletScanResponseModel: {
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'flow_state': 'AT_SITE_PARTIAL',
|
||||
'capacity_packages': 20,
|
||||
'remaining_packages': 10,
|
||||
'allowed_actions': [],
|
||||
},
|
||||
PalletInventoryReportModel: {
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'flow_state': 'FILLED_IN_WAREHOUSE',
|
||||
'initial_packages': 20,
|
||||
'remaining_packages': 20,
|
||||
'updated_at': '2026-07-24T10:00:00',
|
||||
},
|
||||
FillingReportModel: {
|
||||
'filling_id': 1,
|
||||
'filling_no': 'F202607240001',
|
||||
'operated_at': '2026-07-24T10:00:00',
|
||||
'warehouse_id': 1,
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'strain_id': 1,
|
||||
'strain_code_snapshot': 'STRAIN-01',
|
||||
'strain_name_snapshot': '菌种一号',
|
||||
'batch_no': 'B20260724',
|
||||
'package_spec': '20kg/包',
|
||||
'initial_packages': 20,
|
||||
'operator_id': 1,
|
||||
},
|
||||
ShipmentReportModel: {
|
||||
'shipment_id': 1,
|
||||
'shipment_no': 'S202607240001',
|
||||
'confirmed_at': '2026-07-24T12:00:00',
|
||||
'source_warehouse_id': 1,
|
||||
'target_site_id': 1,
|
||||
'customer_id': 1,
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'package_qty': 20,
|
||||
},
|
||||
ConsumptionReportModel: {
|
||||
'usage_id': 1,
|
||||
'usage_no': 'U202607240001',
|
||||
'occurred_at': '2026-07-24T13:00:00',
|
||||
'customer_id': 1,
|
||||
'site_id': 1,
|
||||
'line_id': 1,
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'strain_id': 1,
|
||||
'batch_no': 'B20260724',
|
||||
'used_packages': 1,
|
||||
'remaining_packages_after': 19,
|
||||
'operator_id': 1,
|
||||
},
|
||||
ReturnReportModel: {
|
||||
'return_id': 1,
|
||||
'return_no': 'R202607240001',
|
||||
'status': 'REQUESTED',
|
||||
'requested_at': '2026-07-24T14:00:00',
|
||||
'source_site_id': 1,
|
||||
'target_warehouse_id': 1,
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'remaining_packages_snapshot': 0,
|
||||
},
|
||||
PalletTraceReportModel: {
|
||||
'event_id': 1,
|
||||
'event_seq': 1,
|
||||
'event_type': 'INBOUND_CONFIRMED',
|
||||
'source_type': 'INBOUND',
|
||||
'source_id': 1,
|
||||
'pallet_id': 1,
|
||||
'pallet_code': 'PLT-001',
|
||||
'before_state': 'UNSTOCKED',
|
||||
'after_state': 'EMPTY_IN_WAREHOUSE',
|
||||
'before_remaining_packages': 0,
|
||||
'after_remaining_packages': 0,
|
||||
'operator_id': 1,
|
||||
'occurred_at': '2026-07-24T15:00:00',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class BusinessVoContractTest(unittest.TestCase):
|
||||
def test_all_models_have_exact_fields_and_camel_case_aliases(self) -> None:
|
||||
@@ -684,6 +841,49 @@ class BusinessVoContractTest(unittest.TestCase):
|
||||
with self.subTest(model=model_class.__name__, value=invalid_value), self.assertRaises(ValidationError):
|
||||
model_class.model_validate(payload | {field_name: invalid_value})
|
||||
|
||||
def test_output_occurrence_and_capacity_fields_are_strict_positive_integers(self) -> None:
|
||||
accepted_invalid_values = []
|
||||
|
||||
for model_class, field_names in STRICT_POSITIVE_PACKAGE_FIELDS.items():
|
||||
payload = PACKAGE_MODEL_PAYLOADS[model_class]
|
||||
model_class.model_validate(payload)
|
||||
for field_name in field_names:
|
||||
for invalid_value in (0, '1', 1.5, True):
|
||||
try:
|
||||
model_class.model_validate(payload | {field_name: invalid_value})
|
||||
except ValidationError:
|
||||
continue
|
||||
accepted_invalid_values.append((model_class.__name__, field_name, repr(invalid_value)))
|
||||
|
||||
self.assertEqual([], accepted_invalid_values)
|
||||
self.assertIsNone(
|
||||
ShipmentItemModel.model_validate(
|
||||
PACKAGE_MODEL_PAYLOADS[ShipmentItemModel] | {'package_qty': None}
|
||||
).package_qty
|
||||
)
|
||||
self.assertIsNone(
|
||||
PalletInventoryReportModel.model_validate(
|
||||
PACKAGE_MODEL_PAYLOADS[PalletInventoryReportModel] | {'initial_packages': None}
|
||||
).initial_packages
|
||||
)
|
||||
|
||||
def test_output_snapshot_and_remaining_fields_are_strict_non_negative_integers(self) -> None:
|
||||
accepted_invalid_values = []
|
||||
|
||||
for model_class, field_names in STRICT_NON_NEGATIVE_PACKAGE_FIELDS.items():
|
||||
payload = PACKAGE_MODEL_PAYLOADS[model_class]
|
||||
for field_name in field_names:
|
||||
zero_model = model_class.model_validate(payload | {field_name: 0})
|
||||
self.assertEqual(0, getattr(zero_model, field_name))
|
||||
for invalid_value in (-1, '1', 1.5, True):
|
||||
try:
|
||||
model_class.model_validate(payload | {field_name: invalid_value})
|
||||
except ValidationError:
|
||||
continue
|
||||
accepted_invalid_values.append((model_class.__name__, field_name, repr(invalid_value)))
|
||||
|
||||
self.assertEqual([], accepted_invalid_values)
|
||||
|
||||
def test_id_lists_reject_empty_non_positive_and_duplicate_values(self) -> None:
|
||||
cases = (
|
||||
(DeleteStrainModel, {}, 'strainIds'),
|
||||
|
||||
Reference in New Issue
Block a user