功能:隔离五类测试业务链路

This commit is contained in:
焦龙言
2026-09-23 08:27:14 +08:00
parent 43fe17cc70
commit f5df2150d8
18 changed files with 1206 additions and 831 deletions
@@ -11,6 +11,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import ( from module_business.entity.vo.operation_vo import (
BusinessOptionModel, BusinessOptionModel,
FillingCreateModel, FillingCreateModel,
@@ -40,11 +42,13 @@ async def get_business_filling_list(
filling_page_query: Annotated[FillingPageQueryModel, Query()], filling_page_query: Annotated[FillingPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await FillingService.get_filling_list_services( result = await FillingService.get_filling_list_services(
query_db, query_db,
filling_page_query, filling_page_query,
current_user, current_user,
test_scope,
is_page=True, is_page=True,
) )
return ResponseUtil.success(model_content=result) return ResponseUtil.success(model_content=result)
@@ -61,8 +65,9 @@ async def get_business_filling_strain_options(
request: Request, request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await FillingService.get_strain_options_services(query_db, current_user) result = await FillingService.get_strain_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -81,11 +86,13 @@ async def scan_business_filling_pallet(
], ],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await FillingService.scan_filling_pallet_services( result = await FillingService.scan_filling_pallet_services(
query_db, query_db,
qr_token, qr_token,
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -102,8 +109,9 @@ async def query_detail_business_filling(
filling_id: Annotated[int, Path(gt=0, description='装填记录ID')], filling_id: Annotated[int, Path(gt=0, description='装填记录ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await FillingService.filling_detail_services(query_db, filling_id, current_user) result = await FillingService.filling_detail_services(query_db, filling_id, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -120,6 +128,7 @@ async def add_business_filling(
add_filling: FillingCreateModel, add_filling: FillingCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await FillingService.fill_pallet_services(query_db, add_filling, current_user) result = await FillingService.fill_pallet_services(query_db, add_filling, current_user, test_scope)
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -11,6 +11,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import ( from module_business.entity.vo.operation_vo import (
BusinessOptionModel, BusinessOptionModel,
ConfirmCommandModel, ConfirmCommandModel,
@@ -43,11 +45,13 @@ async def get_business_inbound_list(
inbound_page_query: Annotated[InboundPageQueryModel, Query()], inbound_page_query: Annotated[InboundPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await InboundService.get_inbound_list_services( result = await InboundService.get_inbound_list_services(
query_db, query_db,
inbound_page_query, inbound_page_query,
current_user, current_user,
test_scope,
is_page=True, is_page=True,
) )
return ResponseUtil.success(model_content=result) return ResponseUtil.success(model_content=result)
@@ -64,8 +68,9 @@ async def get_business_inbound_warehouse_options(
request: Request, request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await InboundService.get_warehouse_options_services(query_db, current_user) result = await InboundService.get_warehouse_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -85,12 +90,14 @@ async def scan_business_inbound_pallet(
inbound_scan_query: Annotated[InboundScanQueryModel, Query()], inbound_scan_query: Annotated[InboundScanQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await InboundService.scan_inbound_pallet_services( result = await InboundService.scan_inbound_pallet_services(
query_db, query_db,
qr_token, qr_token,
inbound_scan_query, inbound_scan_query,
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -107,8 +114,9 @@ async def query_detail_business_inbound(
inbound_id: Annotated[int, Path(gt=0, description='入库单ID')], inbound_id: Annotated[int, Path(gt=0, description='入库单ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await InboundService.inbound_detail_services(query_db, inbound_id, current_user) result = await InboundService.inbound_detail_services(query_db, inbound_id, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -125,8 +133,9 @@ async def add_business_inbound(
add_inbound: InboundCreateModel, add_inbound: InboundCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await InboundService.add_inbound_services(query_db, add_inbound, current_user) result = await InboundService.add_inbound_services(query_db, add_inbound, current_user, test_scope)
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -143,8 +152,9 @@ async def edit_business_inbound(
edit_inbound: InboundUpdateModel, edit_inbound: InboundUpdateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await InboundService.edit_inbound_services(query_db, edit_inbound, current_user) result = await InboundService.edit_inbound_services(query_db, edit_inbound, current_user, test_scope)
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -164,11 +174,13 @@ async def delete_business_inbound(
], ],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await InboundService.delete_inbound_services( result = await InboundService.delete_inbound_services(
query_db, query_db,
[int(inbound_id) for inbound_id in inbound_ids.split(',')], [int(inbound_id) for inbound_id in inbound_ids.split(',')],
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -187,11 +199,13 @@ async def confirm_business_inbound(
confirm_command: ConfirmCommandModel, confirm_command: ConfirmCommandModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await InboundService.confirm_inbound_services( result = await InboundService.confirm_inbound_services(
query_db, query_db,
inbound_id, inbound_id,
confirm_command, confirm_command,
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -13,6 +13,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import ( from module_business.entity.vo.operation_vo import (
BusinessOptionModel, BusinessOptionModel,
PalletScanResponseModel, PalletScanResponseModel,
@@ -44,11 +46,13 @@ async def get_business_return_list(
return_page_query: Annotated[ReturnPageQueryModel, Query()], return_page_query: Annotated[ReturnPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ReturnService.get_return_list_services( result = await ReturnService.get_return_list_services(
query_db, query_db,
return_page_query, return_page_query,
current_user, current_user,
test_scope,
is_page=True, is_page=True,
) )
return ResponseUtil.success(model_content=result) return ResponseUtil.success(model_content=result)
@@ -65,8 +69,9 @@ async def get_business_return_warehouse_options(
request: Request, request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ReturnService.get_warehouse_options_services(query_db, current_user) result = await ReturnService.get_warehouse_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -86,12 +91,14 @@ async def scan_business_return_pallet(
return_scan_query: Annotated[ReturnScanQueryModel, Query()], return_scan_query: Annotated[ReturnScanQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ReturnService.scan_return_pallet_services( result = await ReturnService.scan_return_pallet_services(
query_db, query_db,
qr_token, qr_token,
return_scan_query, return_scan_query,
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -109,8 +116,9 @@ async def request_business_return(
request_command: ReturnRequestModel, request_command: ReturnRequestModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await ReturnService.request_return_services(query_db, request_command, current_user) result = await ReturnService.request_return_services(query_db, request_command, current_user, test_scope)
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -126,8 +134,9 @@ async def query_detail_business_return(
return_id: Annotated[int, Path(gt=0, description='退回单ID')], return_id: Annotated[int, Path(gt=0, description='退回单ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ReturnService.return_detail_services(query_db, return_id, current_user) result = await ReturnService.return_detail_services(query_db, return_id, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -145,11 +154,13 @@ async def confirm_business_return(
confirm_command: ReturnConfirmModel, confirm_command: ReturnConfirmModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await ReturnService.confirm_return_services( result = await ReturnService.confirm_return_services(
query_db, query_db,
return_id, return_id,
confirm_command, confirm_command,
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -11,6 +11,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import ( from module_business.entity.vo.operation_vo import (
BusinessOptionModel, BusinessOptionModel,
ConfirmCommandModel, ConfirmCommandModel,
@@ -43,11 +45,13 @@ async def get_business_shipment_list(
shipment_page_query: Annotated[ShipmentPageQueryModel, Query()], shipment_page_query: Annotated[ShipmentPageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ShipmentService.get_shipment_list_services( result = await ShipmentService.get_shipment_list_services(
query_db, query_db,
shipment_page_query, shipment_page_query,
current_user, current_user,
test_scope,
is_page=True, is_page=True,
) )
return ResponseUtil.success(model_content=result) return ResponseUtil.success(model_content=result)
@@ -64,8 +68,9 @@ async def get_business_shipment_warehouse_options(
request: Request, request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ShipmentService.get_warehouse_options_services(query_db, current_user) result = await ShipmentService.get_warehouse_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -80,8 +85,9 @@ async def get_business_shipment_site_options(
request: Request, request: Request,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ShipmentService.get_site_options_services(query_db, current_user) result = await ShipmentService.get_site_options_services(query_db, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -101,12 +107,14 @@ async def scan_business_shipment_pallet(
shipment_scan_query: Annotated[ShipmentScanQueryModel, Query()], shipment_scan_query: Annotated[ShipmentScanQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ShipmentService.scan_shipment_pallet_services( result = await ShipmentService.scan_shipment_pallet_services(
query_db, query_db,
qr_token, qr_token,
shipment_scan_query, shipment_scan_query,
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -123,8 +131,9 @@ async def query_detail_business_shipment(
shipment_id: Annotated[int, Path(gt=0, description='发货单ID')], shipment_id: Annotated[int, Path(gt=0, description='发货单ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await ShipmentService.shipment_detail_services(query_db, shipment_id, current_user) result = await ShipmentService.shipment_detail_services(query_db, shipment_id, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -141,8 +150,9 @@ async def add_business_shipment(
add_shipment: ShipmentCreateModel, add_shipment: ShipmentCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await ShipmentService.add_shipment_services(query_db, add_shipment, current_user) result = await ShipmentService.add_shipment_services(query_db, add_shipment, current_user, test_scope)
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -159,8 +169,9 @@ async def edit_business_shipment(
edit_shipment: ShipmentUpdateModel, edit_shipment: ShipmentUpdateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await ShipmentService.edit_shipment_services(query_db, edit_shipment, current_user) result = await ShipmentService.edit_shipment_services(query_db, edit_shipment, current_user, test_scope)
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -180,11 +191,13 @@ async def delete_business_shipment(
], ],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await ShipmentService.delete_shipment_services( result = await ShipmentService.delete_shipment_services(
query_db, query_db,
[int(shipment_id) for shipment_id in shipment_ids.split(',')], [int(shipment_id) for shipment_id in shipment_ids.split(',')],
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -203,11 +216,13 @@ async def confirm_business_shipment(
confirm_command: ConfirmCommandModel, confirm_command: ConfirmCommandModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await ShipmentService.confirm_shipment_services( result = await ShipmentService.confirm_shipment_services(
query_db, query_db,
shipment_id, shipment_id,
confirm_command, confirm_command,
current_user, current_user,
test_scope,
) )
return ResponseUtil.success(msg=result.message) return ResponseUtil.success(msg=result.message)
@@ -13,6 +13,8 @@ from common.enums import BusinessType
from common.router import APIRouterPro from common.router import APIRouterPro
from common.vo import DataResponseModel, PageResponseModel from common.vo import DataResponseModel, PageResponseModel
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dependency.test_batch_dependency import TestBatchScopeDependency
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.operation_vo import ( from module_business.entity.vo.operation_vo import (
BusinessOptionModel, BusinessOptionModel,
PalletScanResponseModel, PalletScanResponseModel,
@@ -43,11 +45,13 @@ async def get_business_usage_list(
usage_page_query: Annotated[UsagePageQueryModel, Query()], usage_page_query: Annotated[UsagePageQueryModel, Query()],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await UsageService.get_usage_list_services( result = await UsageService.get_usage_list_services(
query_db, query_db,
usage_page_query, usage_page_query,
current_user, current_user,
test_scope,
is_page=True, is_page=True,
) )
return ResponseUtil.success(model_content=result) return ResponseUtil.success(model_content=result)
@@ -65,8 +69,9 @@ async def get_business_usage_line_options(
site_id: Annotated[int, Query(alias='siteId', gt=0, description='客户现场ID')], site_id: Annotated[int, Query(alias='siteId', gt=0, description='客户现场ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await UsageService.get_line_options_services(query_db, site_id, current_user) result = await UsageService.get_line_options_services(query_db, site_id, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -85,8 +90,9 @@ async def scan_business_usage_pallet(
], ],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await UsageService.scan_usage_pallet_services(query_db, qr_token, current_user) result = await UsageService.scan_usage_pallet_services(query_db, qr_token, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -102,8 +108,9 @@ async def query_detail_business_usage(
usage_id: Annotated[int, Path(gt=0, description='使用记录ID')], usage_id: Annotated[int, Path(gt=0, description='使用记录ID')],
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
) -> Response: ) -> Response:
result = await UsageService.usage_detail_services(query_db, usage_id, current_user) result = await UsageService.usage_detail_services(query_db, usage_id, current_user, test_scope)
return ResponseUtil.success(data=result) return ResponseUtil.success(data=result)
@@ -120,6 +127,7 @@ async def add_business_usage(
add_usage: UsageCreateModel, add_usage: UsageCreateModel,
query_db: Annotated[AsyncSession, DBSessionDependency()], query_db: Annotated[AsyncSession, DBSessionDependency()],
current_user: Annotated[CurrentUserModel, CurrentUserDependency()], current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency(write=True)],
) -> Response: ) -> Response:
result = await UsageService.record_usage_services(query_db, add_usage, current_user) result = await UsageService.record_usage_services(query_db, add_usage, current_user, test_scope)
return ResponseUtil.success(msg=result.message, data=result.result) return ResponseUtil.success(msg=result.message, data=result.result)
@@ -1,6 +1,7 @@
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
from module_business.entity.do.master_do import BizPallet from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import BizPalletEvent, BizPalletInventory from module_business.entity.do.operation_do import BizPalletEvent, BizPalletInventory
@@ -13,10 +14,14 @@ class InventoryDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
pallet_id: int, pallet_id: int,
scope: TestBatchScope,
) -> BizPalletInventory | None: ) -> BizPalletInventory | None:
result = await db.execute( result = await db.execute(
select(BizPalletInventory) select(BizPalletInventory)
.where(BizPalletInventory.pallet_id == pallet_id) .where(
BizPalletInventory.pallet_id == pallet_id,
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
.with_for_update() .with_for_update()
.execution_options(populate_existing=True) .execution_options(populate_existing=True)
) )
@@ -27,6 +32,7 @@ class InventoryDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
pallet_ids: list[int], pallet_ids: list[int],
scope: TestBatchScope,
) -> list[BizPalletInventory]: ) -> list[BizPalletInventory]:
ordered_ids = sorted(set(pallet_ids)) ordered_ids = sorted(set(pallet_ids))
if not ordered_ids: if not ordered_ids:
@@ -34,7 +40,10 @@ class InventoryDao:
result = await db.execute( result = await db.execute(
select(BizPalletInventory) select(BizPalletInventory)
.where(BizPalletInventory.pallet_id.in_(ordered_ids)) .where(
BizPalletInventory.pallet_id.in_(ordered_ids),
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
.order_by(BizPalletInventory.pallet_id) .order_by(BizPalletInventory.pallet_id)
.with_for_update() .with_for_update()
.execution_options(populate_existing=True) .execution_options(populate_existing=True)
@@ -46,8 +55,14 @@ class InventoryDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
qr_token: str, qr_token: str,
scope: TestBatchScope,
) -> BizPallet | None: ) -> BizPallet | None:
result = await db.execute(select(BizPallet).where(BizPallet.qr_token == qr_token)) result = await db.execute(
select(BizPallet).where(
BizPallet.qr_token == qr_token,
batch_scope_condition(BizPallet.test_batch_id, scope),
)
)
return result.scalar_one_or_none() return result.scalar_one_or_none()
@classmethod @classmethod
@@ -55,6 +70,7 @@ class InventoryDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
pallet_ids: list[int], pallet_ids: list[int],
scope: TestBatchScope,
) -> list[BizPallet]: ) -> list[BizPallet]:
"""在调用方已锁库存后,按托盘 ID 升序锁定资产行。""" """在调用方已锁库存后,按托盘 ID 升序锁定资产行。"""
ordered_ids = sorted(set(pallet_ids)) ordered_ids = sorted(set(pallet_ids))
@@ -63,7 +79,10 @@ class InventoryDao:
result = await db.execute( result = await db.execute(
select(BizPallet) select(BizPallet)
.where(BizPallet.pallet_id.in_(ordered_ids)) .where(
BizPallet.pallet_id.in_(ordered_ids),
batch_scope_condition(BizPallet.test_batch_id, scope),
)
.order_by(BizPallet.pallet_id) .order_by(BizPallet.pallet_id)
.with_for_update() .with_for_update()
.execution_options(populate_existing=True) .execution_options(populate_existing=True)
@@ -76,11 +95,13 @@ class InventoryDao:
db: AsyncSession, db: AsyncSession,
pallet_id: int, pallet_id: int,
idempotency_key: str, idempotency_key: str,
scope: TestBatchScope,
) -> BizPalletEvent | None: ) -> BizPalletEvent | None:
result = await db.execute( result = await db.execute(
select(BizPalletEvent).where( select(BizPalletEvent).where(
BizPalletEvent.pallet_id == pallet_id, BizPalletEvent.pallet_id == pallet_id,
BizPalletEvent.idempotency_key == idempotency_key, BizPalletEvent.idempotency_key == idempotency_key,
batch_scope_condition(BizPalletEvent.test_batch_id, scope),
) )
) )
return result.scalar_one_or_none() return result.scalar_one_or_none()
@@ -7,6 +7,7 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel from common.vo import PageModel
from module_business.domain.enums import DocumentStatus, PalletFlowState from module_business.domain.enums import DocumentStatus, PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
from module_business.entity.do.master_do import BizPallet from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
@@ -56,36 +57,49 @@ class OperationDao:
@staticmethod @staticmethod
def _inbound_scope_conditions( def _inbound_scope_conditions(
warehouse_ids: list[int] | None, warehouse_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]: ) -> list[ColumnElement[bool]]:
if warehouse_ids is None: if warehouse_ids is None:
return [] return [batch_scope_condition(BizInboundOrder.test_batch_id, scope)]
scoped_warehouse_ids = sorted(set(warehouse_ids)) scoped_warehouse_ids = sorted(set(warehouse_ids))
return [BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()] return [
batch_scope_condition(BizInboundOrder.test_batch_id, scope),
BizInboundOrder.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false(),
]
@staticmethod @staticmethod
def _filling_scope_conditions( def _filling_scope_conditions(
warehouse_ids: list[int] | None, warehouse_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]: ) -> list[ColumnElement[bool]]:
if warehouse_ids is None: if warehouse_ids is None:
return [] return [batch_scope_condition(BizFillingRecord.test_batch_id, scope)]
scoped_warehouse_ids = sorted(set(warehouse_ids)) scoped_warehouse_ids = sorted(set(warehouse_ids))
return [BizFillingRecord.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()] return [
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
BizFillingRecord.warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false(),
]
@staticmethod @staticmethod
def _usage_scope_conditions( def _usage_scope_conditions(
site_ids: list[int] | None, site_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]: ) -> list[ColumnElement[bool]]:
if site_ids is None: if site_ids is None:
return [] return [batch_scope_condition(BizUsageRecord.test_batch_id, scope)]
scoped_site_ids = sorted(set(site_ids)) scoped_site_ids = sorted(set(site_ids))
return [BizUsageRecord.site_id.in_(scoped_site_ids) if scoped_site_ids else false()] return [
batch_scope_condition(BizUsageRecord.test_batch_id, scope),
BizUsageRecord.site_id.in_(scoped_site_ids) if scoped_site_ids else false(),
]
@staticmethod @staticmethod
def _shipment_scope_conditions( def _shipment_scope_conditions(
warehouse_ids: list[int] | None, warehouse_ids: list[int] | None,
site_ids: list[int] | None, site_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]: ) -> list[ColumnElement[bool]]:
conditions = [] conditions = [batch_scope_condition(BizShipmentOrder.test_batch_id, scope)]
if warehouse_ids is not None: if warehouse_ids is not None:
scoped_warehouse_ids = sorted(set(warehouse_ids)) scoped_warehouse_ids = sorted(set(warehouse_ids))
conditions.append( conditions.append(
@@ -100,9 +114,10 @@ class OperationDao:
def _return_scope_conditions( def _return_scope_conditions(
site_ids: list[int] | None, site_ids: list[int] | None,
warehouse_ids: list[int] | None, warehouse_ids: list[int] | None,
scope: TestBatchScope,
) -> list[ColumnElement[bool]]: ) -> list[ColumnElement[bool]]:
if site_ids is None and warehouse_ids is None: if site_ids is None and warehouse_ids is None:
return [] return [batch_scope_condition(BizReturnOrder.test_batch_id, scope)]
visibility_conditions: list[ColumnElement[bool]] = [] visibility_conditions: list[ColumnElement[bool]] = []
if site_ids is not None: if site_ids is not None:
@@ -115,7 +130,7 @@ class OperationDao:
visibility_conditions.append( visibility_conditions.append(
BizReturnOrder.target_warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false() BizReturnOrder.target_warehouse_id.in_(scoped_warehouse_ids) if scoped_warehouse_ids else false()
) )
return [or_(*visibility_conditions)] return [batch_scope_condition(BizReturnOrder.test_batch_id, scope), or_(*visibility_conditions)]
@staticmethod @staticmethod
async def _paginate( async def _paginate(
@@ -180,6 +195,7 @@ class OperationDao:
item_model: type[Any], item_model: type[Any],
item_order_id_column: InstrumentedAttribute, item_order_id_column: InstrumentedAttribute,
item_id_column: InstrumentedAttribute, item_id_column: InstrumentedAttribute,
scope: TestBatchScope,
*order_conditions: ColumnElement[bool], *order_conditions: ColumnElement[bool],
) -> Any | None: ) -> Any | None:
order = await cls._get_by_id( order = await cls._get_by_id(
@@ -193,7 +209,9 @@ class OperationDao:
return None return None
item_result = await db.execute( item_result = await db.execute(
select(item_model).where(item_order_id_column == order_id).order_by(item_id_column) select(item_model)
.where(item_order_id_column == order_id, batch_scope_condition(item_model.test_batch_id, scope))
.order_by(item_id_column)
) )
order.items = list(item_result.scalars().all()) order.items = list(item_result.scalars().all())
return order return order
@@ -260,6 +278,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
inbound_id: int, inbound_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None: ) -> BizInboundOrder | None:
inbound = await cls._get_for_update( inbound = await cls._get_for_update(
@@ -267,7 +286,7 @@ class OperationDao:
BizInboundOrder, BizInboundOrder,
BizInboundOrder.inbound_id, BizInboundOrder.inbound_id,
inbound_id, inbound_id,
*cls._inbound_scope_conditions(warehouse_ids), *cls._inbound_scope_conditions(warehouse_ids, scope),
) )
if inbound is None: if inbound is None:
return None return None
@@ -275,7 +294,10 @@ class OperationDao:
# 父单锁定后用 locking read 获取最新明细,避免 MySQL RR 继续读取确认前的旧快照。 # 父单锁定后用 locking read 获取最新明细,避免 MySQL RR 继续读取确认前的旧快照。
item_result = await db.execute( item_result = await db.execute(
select(BizInboundItem) select(BizInboundItem)
.where(BizInboundItem.inbound_id == inbound_id) .where(
BizInboundItem.inbound_id == inbound_id,
batch_scope_condition(BizInboundItem.test_batch_id, scope),
)
.order_by(BizInboundItem.pallet_id, BizInboundItem.inbound_item_id) .order_by(BizInboundItem.pallet_id, BizInboundItem.inbound_item_id)
.with_for_update() .with_for_update()
.execution_options(populate_existing=True) .execution_options(populate_existing=True)
@@ -291,6 +313,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
shipment_id: int, shipment_id: int,
scope: TestBatchScope,
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:
@@ -299,7 +322,7 @@ class OperationDao:
BizShipmentOrder, BizShipmentOrder,
BizShipmentOrder.shipment_id, BizShipmentOrder.shipment_id,
shipment_id, shipment_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids), *cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
) )
if shipment is None: if shipment is None:
return None return None
@@ -307,7 +330,10 @@ class OperationDao:
# 父单锁定后读取当前明细,避免 MySQL RR 沿用确认前的快照。 # 父单锁定后读取当前明细,避免 MySQL RR 沿用确认前的快照。
item_result = await db.execute( item_result = await db.execute(
select(BizShipmentItem) select(BizShipmentItem)
.where(BizShipmentItem.shipment_id == shipment_id) .where(
BizShipmentItem.shipment_id == shipment_id,
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.pallet_id, BizShipmentItem.shipment_item_id) .order_by(BizShipmentItem.pallet_id, BizShipmentItem.shipment_item_id)
.with_for_update() .with_for_update()
.execution_options(populate_existing=True) .execution_options(populate_existing=True)
@@ -323,6 +349,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
return_id: int, return_id: int,
scope: TestBatchScope,
site_ids: list[int] | None = None, site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None: ) -> BizReturnOrder | None:
@@ -331,7 +358,7 @@ class OperationDao:
BizReturnOrder, BizReturnOrder,
BizReturnOrder.return_id, BizReturnOrder.return_id,
return_id, return_id,
*cls._return_scope_conditions(site_ids, warehouse_ids), *cls._return_scope_conditions(site_ids, warehouse_ids, scope),
) )
if return_order is None: if return_order is None:
return None return None
@@ -339,7 +366,10 @@ class OperationDao:
# 父单锁定后使用 locking read 获取当前明细,避免 MySQL RR 沿用旧快照。 # 父单锁定后使用 locking read 获取当前明细,避免 MySQL RR 沿用旧快照。
item_result = await db.execute( item_result = await db.execute(
select(BizReturnItem) select(BizReturnItem)
.where(BizReturnItem.return_id == return_id) .where(
BizReturnItem.return_id == return_id,
batch_scope_condition(BizReturnItem.test_batch_id, scope),
)
.order_by(BizReturnItem.pallet_id, BizReturnItem.return_item_id) .order_by(BizReturnItem.pallet_id, BizReturnItem.return_item_id)
.with_for_update() .with_for_update()
.execution_options(populate_existing=True) .execution_options(populate_existing=True)
@@ -355,6 +385,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
filling_id: int, filling_id: int,
scope: TestBatchScope,
) -> BizFillingRecord | None: ) -> BizFillingRecord | None:
"""按已锁库存的 active_filling_id 锁定仍处于活动状态的装填周期。""" """按已锁库存的 active_filling_id 锁定仍处于活动状态的装填周期。"""
return await cls._get_for_update( return await cls._get_for_update(
@@ -362,6 +393,7 @@ class OperationDao:
BizFillingRecord, BizFillingRecord,
BizFillingRecord.filling_id, BizFillingRecord.filling_id,
filling_id, filling_id,
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
BizFillingRecord.active_slot == 'ACTIVE', BizFillingRecord.active_slot == 'ACTIVE',
) )
@@ -370,6 +402,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
idempotency_key: str, idempotency_key: str,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None: ) -> BizInboundOrder | None:
return await cls._get_by_key( return await cls._get_by_key(
@@ -377,7 +410,7 @@ class OperationDao:
BizInboundOrder, BizInboundOrder,
BizInboundOrder.confirm_idempotency_key, BizInboundOrder.confirm_idempotency_key,
idempotency_key, idempotency_key,
*cls._inbound_scope_conditions(warehouse_ids), *cls._inbound_scope_conditions(warehouse_ids, scope),
) )
@classmethod @classmethod
@@ -385,6 +418,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
idempotency_key: str, idempotency_key: str,
scope: TestBatchScope,
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:
@@ -393,7 +427,7 @@ class OperationDao:
BizShipmentOrder, BizShipmentOrder,
BizShipmentOrder.confirm_idempotency_key, BizShipmentOrder.confirm_idempotency_key,
idempotency_key, idempotency_key,
*cls._shipment_scope_conditions(warehouse_ids, site_ids), *cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
) )
@classmethod @classmethod
@@ -401,6 +435,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
idempotency_key: str, idempotency_key: str,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizFillingRecord | None: ) -> BizFillingRecord | None:
return await cls._get_by_key( return await cls._get_by_key(
@@ -408,7 +443,7 @@ class OperationDao:
BizFillingRecord, BizFillingRecord,
BizFillingRecord.idempotency_key, BizFillingRecord.idempotency_key,
idempotency_key, idempotency_key,
*cls._filling_scope_conditions(warehouse_ids), *cls._filling_scope_conditions(warehouse_ids, scope),
) )
@classmethod @classmethod
@@ -416,6 +451,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
idempotency_key: str, idempotency_key: str,
scope: TestBatchScope,
site_ids: list[int] | None = None, site_ids: list[int] | None = None,
) -> BizUsageRecord | None: ) -> BizUsageRecord | None:
return await cls._get_by_key( return await cls._get_by_key(
@@ -423,7 +459,7 @@ class OperationDao:
BizUsageRecord, BizUsageRecord,
BizUsageRecord.idempotency_key, BizUsageRecord.idempotency_key,
idempotency_key, idempotency_key,
*cls._usage_scope_conditions(site_ids), *cls._usage_scope_conditions(site_ids, scope),
) )
@classmethod @classmethod
@@ -433,6 +469,7 @@ class OperationDao:
idempotency_key: str, idempotency_key: str,
source_site_id: int, source_site_id: int,
target_warehouse_id: int, target_warehouse_id: int,
scope: TestBatchScope,
) -> BizReturnOrder | None: ) -> BizReturnOrder | None:
return await cls._get_by_key( return await cls._get_by_key(
db, db,
@@ -441,6 +478,7 @@ class OperationDao:
idempotency_key, idempotency_key,
BizReturnOrder.source_site_id == source_site_id, BizReturnOrder.source_site_id == source_site_id,
BizReturnOrder.target_warehouse_id == target_warehouse_id, BizReturnOrder.target_warehouse_id == target_warehouse_id,
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
) )
@classmethod @classmethod
@@ -450,6 +488,7 @@ class OperationDao:
idempotency_key: str, idempotency_key: str,
source_site_id: int, source_site_id: int,
target_warehouse_id: int, target_warehouse_id: int,
scope: TestBatchScope,
) -> BizReturnOrder | None: ) -> BizReturnOrder | None:
return await cls._get_by_key( return await cls._get_by_key(
db, db,
@@ -458,6 +497,7 @@ class OperationDao:
idempotency_key, idempotency_key,
BizReturnOrder.source_site_id == source_site_id, BizReturnOrder.source_site_id == source_site_id,
BizReturnOrder.target_warehouse_id == target_warehouse_id, BizReturnOrder.target_warehouse_id == target_warehouse_id,
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
) )
@classmethod @classmethod
@@ -465,10 +505,11 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
query_object: InboundPageQueryModel, query_object: InboundPageQueryModel,
warehouse_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]: ) -> PageModel | list[dict[str, Any]]:
conditions = cls._inbound_scope_conditions(warehouse_ids) conditions = cls._inbound_scope_conditions(warehouse_ids, scope)
if query_object.inbound_no: if query_object.inbound_no:
conditions.append(BizInboundOrder.inbound_no.like(f'%{query_object.inbound_no}%')) conditions.append(BizInboundOrder.inbound_no.like(f'%{query_object.inbound_no}%'))
if query_object.warehouse_id: if query_object.warehouse_id:
@@ -490,6 +531,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
inbound_id: int, inbound_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None: ) -> BizInboundOrder | None:
return await cls._get_order_detail( return await cls._get_order_detail(
@@ -500,7 +542,8 @@ class OperationDao:
BizInboundItem, BizInboundItem,
BizInboundItem.inbound_id, BizInboundItem.inbound_id,
BizInboundItem.inbound_item_id, BizInboundItem.inbound_item_id,
*cls._inbound_scope_conditions(warehouse_ids), scope,
*cls._inbound_scope_conditions(warehouse_ids, scope),
) )
@classmethod @classmethod
@@ -549,10 +592,11 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
query_object: FillingPageQueryModel, query_object: FillingPageQueryModel,
warehouse_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]: ) -> PageModel | list[dict[str, Any]]:
conditions = cls._filling_scope_conditions(warehouse_ids) conditions = cls._filling_scope_conditions(warehouse_ids, scope)
if query_object.filling_no: if query_object.filling_no:
conditions.append(BizFillingRecord.filling_no.like(f'%{query_object.filling_no}%')) conditions.append(BizFillingRecord.filling_no.like(f'%{query_object.filling_no}%'))
if query_object.warehouse_id: if query_object.warehouse_id:
@@ -584,6 +628,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
filling_id: int, filling_id: int,
scope: TestBatchScope,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizFillingRecord | None: ) -> BizFillingRecord | None:
return await cls._get_by_id( return await cls._get_by_id(
@@ -591,7 +636,7 @@ class OperationDao:
BizFillingRecord, BizFillingRecord,
BizFillingRecord.filling_id, BizFillingRecord.filling_id,
filling_id, filling_id,
*cls._filling_scope_conditions(warehouse_ids), *cls._filling_scope_conditions(warehouse_ids, scope),
) )
@classmethod @classmethod
@@ -620,11 +665,12 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
query_object: ShipmentPageQueryModel, query_object: ShipmentPageQueryModel,
warehouse_ids: list[int] | None,
site_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]: ) -> PageModel | list[dict[str, Any]]:
conditions = cls._shipment_scope_conditions(warehouse_ids, site_ids) conditions = cls._shipment_scope_conditions(warehouse_ids, site_ids, scope)
if query_object.shipment_no: if query_object.shipment_no:
conditions.append(BizShipmentOrder.shipment_no.like(f'%{query_object.shipment_no}%')) conditions.append(BizShipmentOrder.shipment_no.like(f'%{query_object.shipment_no}%'))
if query_object.source_warehouse_id: if query_object.source_warehouse_id:
@@ -648,6 +694,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
shipment_id: int, shipment_id: int,
scope: TestBatchScope,
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:
@@ -656,7 +703,7 @@ class OperationDao:
BizShipmentOrder, BizShipmentOrder,
BizShipmentOrder.shipment_id, BizShipmentOrder.shipment_id,
shipment_id, shipment_id,
*cls._shipment_scope_conditions(warehouse_ids, site_ids), *cls._shipment_scope_conditions(warehouse_ids, site_ids, scope),
) )
if shipment is None: if shipment is None:
return None return None
@@ -674,9 +721,13 @@ class OperationDao:
BizPalletInventory.current_line_id.is_(None), BizPalletInventory.current_line_id.is_(None),
BizPalletInventory.active_filling_id.is_not(None), BizPalletInventory.active_filling_id.is_not(None),
BizPalletInventory.remaining_packages > 0, BizPalletInventory.remaining_packages > 0,
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
), ),
) )
.where(BizShipmentItem.shipment_id == shipment_id) .where(
BizShipmentItem.shipment_id == shipment_id,
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.shipment_item_id) .order_by(BizShipmentItem.shipment_item_id)
) )
items = [] items = []
@@ -686,7 +737,10 @@ class OperationDao:
else: else:
item_result = await db.execute( item_result = await db.execute(
select(BizShipmentItem) select(BizShipmentItem)
.where(BizShipmentItem.shipment_id == shipment_id) .where(
BizShipmentItem.shipment_id == shipment_id,
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
)
.order_by(BizShipmentItem.shipment_item_id) .order_by(BizShipmentItem.shipment_item_id)
) )
items = list(item_result.scalars().all()) items = list(item_result.scalars().all())
@@ -746,10 +800,11 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
query_object: UsagePageQueryModel, query_object: UsagePageQueryModel,
site_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
site_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]: ) -> PageModel | list[dict[str, Any]]:
conditions = cls._usage_scope_conditions(site_ids) conditions = cls._usage_scope_conditions(site_ids, scope)
if query_object.usage_no: if query_object.usage_no:
conditions.append(BizUsageRecord.usage_no.like(f'%{query_object.usage_no}%')) conditions.append(BizUsageRecord.usage_no.like(f'%{query_object.usage_no}%'))
if query_object.site_id: if query_object.site_id:
@@ -779,6 +834,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
usage_id: int, usage_id: int,
scope: TestBatchScope,
site_ids: list[int] | None = None, site_ids: list[int] | None = None,
) -> BizUsageRecord | None: ) -> BizUsageRecord | None:
return await cls._get_by_id( return await cls._get_by_id(
@@ -786,7 +842,7 @@ class OperationDao:
BizUsageRecord, BizUsageRecord,
BizUsageRecord.usage_id, BizUsageRecord.usage_id,
usage_id, usage_id,
*cls._usage_scope_conditions(site_ids), *cls._usage_scope_conditions(site_ids, scope),
) )
@classmethod @classmethod
@@ -802,11 +858,12 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
query_object: ReturnPageQueryModel, query_object: ReturnPageQueryModel,
site_ids: list[int] | None,
warehouse_ids: list[int] | None,
scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None,
) -> PageModel | list[dict[str, Any]]: ) -> PageModel | list[dict[str, Any]]:
conditions = cls._return_scope_conditions(site_ids, warehouse_ids) conditions = cls._return_scope_conditions(site_ids, warehouse_ids, scope)
if query_object.return_no: if query_object.return_no:
conditions.append(BizReturnOrder.return_no.like(f'%{query_object.return_no}%')) conditions.append(BizReturnOrder.return_no.like(f'%{query_object.return_no}%'))
if query_object.source_site_id: if query_object.source_site_id:
@@ -830,6 +887,7 @@ class OperationDao:
cls, cls,
db: AsyncSession, db: AsyncSession,
return_id: int, return_id: int,
scope: TestBatchScope,
site_ids: list[int] | None = None, site_ids: list[int] | None = None,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizReturnOrder | None: ) -> BizReturnOrder | None:
@@ -841,7 +899,8 @@ class OperationDao:
BizReturnItem, BizReturnItem,
BizReturnItem.return_id, BizReturnItem.return_id,
BizReturnItem.return_item_id, BizReturnItem.return_item_id,
*cls._return_scope_conditions(site_ids, warehouse_ids), scope,
*cls._return_scope_conditions(site_ids, warehouse_ids, scope),
) )
@classmethod @classmethod
@@ -16,6 +16,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet, BizStrain from module_business.entity.do.master_do import BizPallet, BizStrain
from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory
from module_business.entity.vo.master_vo import StrainPageQueryModel from module_business.entity.vo.master_vo import StrainPageQueryModel
@@ -27,6 +28,7 @@ from module_business.entity.vo.operation_vo import (
PalletScanResponseModel, PalletScanResponseModel,
) )
from module_business.service.access_service import AccessService from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class FillingService: class FillingService:
@@ -180,6 +182,7 @@ class FillingService:
query_db: AsyncSession, query_db: AsyncSession,
query_object: FillingPageQueryModel, query_object: FillingPageQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
) -> PageModel | list[FillingModel]: ) -> PageModel | list[FillingModel]:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -192,8 +195,9 @@ class FillingService:
result = await OperationDao.get_filling_list( result = await OperationDao.get_filling_list(
query_db, query_db,
query_object, query_object,
is_page,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
scope=test_scope,
is_page=is_page,
) )
rows = result.rows if isinstance(result, PageModel) else result rows = result.rows if isinstance(result, PageModel) else result
models = [FillingModel.model_validate(record) for record in rows] models = [FillingModel.model_validate(record) for record in rows]
@@ -207,11 +211,13 @@ class FillingService:
query_db: AsyncSession, query_db: AsyncSession,
filling_id: int, filling_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> FillingModel: ) -> FillingModel:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
filling = await OperationDao.get_filling_by_id( filling = await OperationDao.get_filling_by_id(
query_db, query_db,
filling_id, filling_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if filling is None: if filling is None:
@@ -223,11 +229,13 @@ class FillingService:
cls, cls,
query_db: AsyncSession, query_db: AsyncSession,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]: ) -> list[BusinessOptionModel]:
cls._current_identity(current_user) cls._current_identity(current_user)
strains = await MasterDao.list_strain( strains = await MasterDao.list_strain(
query_db, query_db,
StrainPageQueryModel(status='0'), StrainPageQueryModel(status='0'),
test_scope,
False, False,
) )
options = [ options = [
@@ -247,11 +255,12 @@ class FillingService:
query_db: AsyncSession, query_db: AsyncSession,
qr_token: str, qr_token: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel: ) -> PalletScanResponseModel:
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token) pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None: if pallet is None:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id) inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None: if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.warehouse_id is None: if inventory.warehouse_id is None:
@@ -292,6 +301,7 @@ class FillingService:
page_object: FillingCreateModel, page_object: FillingCreateModel,
request_hash: str, request_hash: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -300,6 +310,7 @@ class FillingService:
recovered = await OperationDao.get_filling_by_idempotency_key( recovered = await OperationDao.get_filling_by_idempotency_key(
query_db, query_db,
page_object.idempotency_key, page_object.idempotency_key,
test_scope,
warehouse_ids=[page_object.warehouse_id], warehouse_ids=[page_object.warehouse_id],
) )
if recovered is None: if recovered is None:
@@ -321,9 +332,11 @@ class FillingService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: FillingCreateModel, page_object: FillingCreateModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
recovery_transaction_ended = False recovery_transaction_ended = False
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
request_hash = cls._build_request_hash(page_object) request_hash = cls._build_request_hash(page_object)
user_id, user_name = cls._current_identity(current_user) user_id, user_name = cls._current_identity(current_user)
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -333,6 +346,7 @@ class FillingService:
existing = await OperationDao.get_filling_by_idempotency_key( existing = await OperationDao.get_filling_by_idempotency_key(
query_db, query_db,
page_object.idempotency_key, page_object.idempotency_key,
test_scope,
warehouse_ids=[page_object.warehouse_id], warehouse_ids=[page_object.warehouse_id],
) )
if existing is not None: if existing is not None:
@@ -356,6 +370,7 @@ class FillingService:
scanned_pallet = await InventoryDao.get_pallet_by_qr_token( scanned_pallet = await InventoryDao.get_pallet_by_qr_token(
query_db, query_db,
page_object.qr_token, page_object.qr_token,
test_scope,
) )
if scanned_pallet is None: if scanned_pallet is None:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
@@ -363,6 +378,7 @@ class FillingService:
inventory = await InventoryDao.get_inventory_for_update( inventory = await InventoryDao.get_inventory_for_update(
query_db, query_db,
scanned_pallet.pallet_id, scanned_pallet.pallet_id,
test_scope,
) )
if inventory is None: if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
@@ -376,6 +392,7 @@ class FillingService:
locked_pallets = await InventoryDao.get_pallets_by_ids( locked_pallets = await InventoryDao.get_pallets_by_ids(
query_db, query_db,
[scanned_pallet.pallet_id], [scanned_pallet.pallet_id],
test_scope,
) )
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != scanned_pallet.pallet_id: if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != scanned_pallet.pallet_id:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
@@ -385,6 +402,7 @@ class FillingService:
concurrent = await OperationDao.get_active_filling_for_update( concurrent = await OperationDao.get_active_filling_for_update(
query_db, query_db,
inventory.active_filling_id, inventory.active_filling_id,
test_scope,
) )
if concurrent is not None and concurrent.idempotency_key == page_object.idempotency_key: if concurrent is not None and concurrent.idempotency_key == page_object.idempotency_key:
# 结束首次非锁定读取建立的 RR 快照,再按最新数据范围恢复并发成功结果。 # 结束首次非锁定读取建立的 RR 快照,再按最新数据范围恢复并发成功结果。
@@ -395,11 +413,13 @@ class FillingService:
page_object, page_object,
request_hash, request_hash,
current_user, current_user,
test_scope,
) )
warehouse = await MasterDao.get_warehouse( warehouse = await MasterDao.get_warehouse(
query_db, query_db,
inventory.warehouse_id, inventory.warehouse_id,
test_scope,
for_update=True, for_update=True,
) )
if warehouse is None: if warehouse is None:
@@ -416,6 +436,7 @@ class FillingService:
await MasterDao.get_strain( await MasterDao.get_strain(
query_db, query_db,
page_object.strain_id, page_object.strain_id,
test_scope,
for_update=True, for_update=True,
) )
) )
@@ -452,6 +473,7 @@ class FillingService:
create_time=current_time, create_time=current_time,
update_by=user_name, update_by=user_name,
update_time=current_time, update_time=current_time,
test_batch_id=test_scope.batch_id,
) )
filling = await OperationDao.add_filling_record(query_db, filling) filling = await OperationDao.add_filling_record(query_db, filling)
filling_id = filling.filling_id filling_id = filling.filling_id
@@ -485,6 +507,7 @@ class FillingService:
occurred_at=current_time, occurred_at=current_time,
idempotency_key=page_object.idempotency_key, idempotency_key=page_object.idempotency_key,
request_hash=request_hash, request_hash=request_hash,
test_batch_id=test_scope.batch_id,
) )
await InventoryDao.add_event(query_db, inventory, event) await InventoryDao.add_event(query_db, inventory, event)
await query_db.commit() await query_db.commit()
@@ -496,6 +519,7 @@ class FillingService:
page_object, page_object,
request_hash, request_hash,
current_user, current_user,
test_scope,
) )
except Exception: except Exception:
if not recovery_transaction_ended: if not recovery_transaction_ended:
@@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel from common.vo import CrudResponseModel, PageModel
from exceptions.exception import PermissionException, ServiceException from exceptions.exception import PermissionException, ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.access_dao import AccessDao from module_business.dao.access_dao import AccessDao
from module_business.dao.inventory_dao import InventoryDao from module_business.dao.inventory_dao import InventoryDao
@@ -16,6 +16,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizInboundItem, BizInboundItem,
@@ -35,6 +36,7 @@ from module_business.entity.vo.operation_vo import (
PalletScanResponseModel, PalletScanResponseModel,
) )
from module_business.service.access_service import AccessService from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class InboundService: class InboundService:
@@ -95,9 +97,10 @@ class InboundService:
query_db: AsyncSession, query_db: AsyncSession,
warehouse_id: int, warehouse_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> Any: ) -> Any:
await AccessService.assert_warehouse_access(query_db, current_user, warehouse_id) await AccessService.assert_warehouse_access(query_db, current_user, warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, warehouse_id) warehouse = await MasterDao.get_warehouse(query_db, warehouse_id, test_scope)
if warehouse is None: if warehouse is None:
raise ServiceException(message='目标仓库不存在') raise ServiceException(message='目标仓库不存在')
if warehouse.status != '0': if warehouse.status != '0':
@@ -113,6 +116,7 @@ class InboundService:
warehouse_ids: set[int], warehouse_ids: set[int],
hidden_warehouse_ids: set[int], hidden_warehouse_ids: set[int],
require_active: bool, require_active: bool,
test_scope: TestBatchScope,
) -> None: ) -> None:
"""锁定仓库及授权映射,并基于锁后最新状态重新校验写权限。""" """锁定仓库及授权映射,并基于锁后最新状态重新校验写权限。"""
warehouses: dict[int, Any] = {} warehouses: dict[int, Any] = {}
@@ -121,6 +125,7 @@ class InboundService:
warehouses[warehouse_id] = await MasterDao.get_warehouse( warehouses[warehouse_id] = await MasterDao.get_warehouse(
query_db, query_db,
warehouse_id, warehouse_id,
test_scope,
for_update=True, for_update=True,
) )
try: try:
@@ -152,6 +157,7 @@ class InboundService:
inbound: BizInboundOrder, inbound: BizInboundOrder,
current_user: CurrentUserModel, current_user: CurrentUserModel,
*, *,
test_scope: TestBatchScope,
require_active: bool = True, require_active: bool = True,
) -> None: ) -> None:
await cls._lock_and_authorize_warehouses( await cls._lock_and_authorize_warehouses(
@@ -160,6 +166,7 @@ class InboundService:
warehouse_ids={inbound.warehouse_id}, warehouse_ids={inbound.warehouse_id},
hidden_warehouse_ids={inbound.warehouse_id}, hidden_warehouse_ids={inbound.warehouse_id},
require_active=require_active, require_active=require_active,
test_scope=test_scope,
) )
@staticmethod @staticmethod
@@ -183,10 +190,11 @@ class InboundService:
cls, cls,
query_db: AsyncSession, query_db: AsyncSession,
pallet_ids: list[int], pallet_ids: list[int],
test_scope: TestBatchScope,
) -> tuple[list[BizPalletInventory], list[BizPallet]]: ) -> tuple[list[BizPalletInventory], list[BizPallet]]:
ordered_ids = sorted(pallet_ids) ordered_ids = sorted(pallet_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids) inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids) pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
expected_ids = set(ordered_ids) expected_ids = set(ordered_ids)
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories} inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets} pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
@@ -205,6 +213,7 @@ class InboundService:
pallet_ids: list[int], pallet_ids: list[int],
user_name: str, user_name: str,
current_time: datetime, current_time: datetime,
test_batch_id: int | None,
) -> list[BizInboundItem]: ) -> list[BizInboundItem]:
return [ return [
BizInboundItem( BizInboundItem(
@@ -212,6 +221,7 @@ class InboundService:
pallet_id=pallet_id, pallet_id=pallet_id,
create_by=user_name, create_by=user_name,
create_time=current_time, create_time=current_time,
test_batch_id=test_batch_id,
) )
for pallet_id in sorted(pallet_ids) for pallet_id in sorted(pallet_ids)
] ]
@@ -226,6 +236,7 @@ class InboundService:
query_db: AsyncSession, query_db: AsyncSession,
query_object: InboundPageQueryModel, query_object: InboundPageQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
) -> PageModel | list[InboundModel]: ) -> PageModel | list[InboundModel]:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -239,8 +250,9 @@ class InboundService:
result = await OperationDao.get_inbound_list( result = await OperationDao.get_inbound_list(
query_db, query_db,
query_object, query_object,
is_page,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
scope=test_scope,
is_page=is_page,
) )
rows = result.rows if isinstance(result, PageModel) else result rows = result.rows if isinstance(result, PageModel) else result
models = [InboundModel.model_validate(record) for record in rows] models = [InboundModel.model_validate(record) for record in rows]
@@ -254,11 +266,13 @@ class InboundService:
query_db: AsyncSession, query_db: AsyncSession,
inbound_id: int, inbound_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> InboundModel: ) -> InboundModel:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
inbound = await OperationDao.get_inbound_by_id( inbound = await OperationDao.get_inbound_by_id(
query_db, query_db,
inbound_id, inbound_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if inbound is None: if inbound is None:
@@ -270,12 +284,14 @@ class InboundService:
cls, cls,
query_db: AsyncSession, query_db: AsyncSession,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]: ) -> list[BusinessOptionModel]:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
allowed_ids = None if warehouse_ids is None else set(warehouse_ids) allowed_ids = None if warehouse_ids is None else set(warehouse_ids)
warehouses = await MasterDao.list_warehouse( warehouses = await MasterDao.list_warehouse(
query_db, query_db,
WarehousePageQueryModel(status='0'), WarehousePageQueryModel(status='0'),
test_scope,
False, False,
) )
options = [] options = []
@@ -301,22 +317,23 @@ class InboundService:
qr_token: str, qr_token: str,
query_object: InboundScanQueryModel, query_object: InboundScanQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel: ) -> PalletScanResponseModel:
await AccessService.assert_warehouse_access( await AccessService.assert_warehouse_access(
query_db, query_db,
current_user, current_user,
query_object.warehouse_id, query_object.warehouse_id,
) )
warehouse = await MasterDao.get_warehouse(query_db, query_object.warehouse_id) warehouse = await MasterDao.get_warehouse(query_db, query_object.warehouse_id, test_scope)
if warehouse is None: if warehouse is None:
raise ServiceException(message='目标仓库不存在') raise ServiceException(message='目标仓库不存在')
if warehouse.status != '0': if warehouse.status != '0':
raise ServiceException(message='目标仓库已停用') raise ServiceException(message='目标仓库已停用')
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token) pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None: if pallet is None:
raise ServiceException(message='托盘不存在') raise ServiceException(message='托盘不存在')
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id) inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None: if inventory is None:
raise ServiceException(message='托盘库存不存在') raise ServiceException(message='托盘库存不存在')
cls._validate_pallet_pair(pallet, inventory) cls._validate_pallet_pair(pallet, inventory)
@@ -344,22 +361,26 @@ class InboundService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: InboundCreateModel, page_object: InboundCreateModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user) _, user_name = cls._current_identity(current_user)
await cls._validate_active_warehouse( await cls._validate_active_warehouse(
query_db, query_db,
page_object.warehouse_id, page_object.warehouse_id,
current_user, current_user,
test_scope,
) )
ordered_ids = sorted(page_object.pallet_ids) ordered_ids = sorted(page_object.pallet_ids)
await cls._lock_and_validate_pallets(query_db, ordered_ids) await cls._lock_and_validate_pallets(query_db, ordered_ids, test_scope)
await cls._lock_and_authorize_warehouses( await cls._lock_and_authorize_warehouses(
query_db, query_db,
current_user, current_user,
warehouse_ids={page_object.warehouse_id}, warehouse_ids={page_object.warehouse_id},
hidden_warehouse_ids=set(), hidden_warehouse_ids=set(),
require_active=True, require_active=True,
test_scope=test_scope,
) )
current_time = datetime.now() current_time = datetime.now()
@@ -371,12 +392,13 @@ class InboundService:
create_time=current_time, create_time=current_time,
update_by=user_name, update_by=user_name,
update_time=current_time, update_time=current_time,
test_batch_id=test_scope.batch_id,
) )
inbound = await OperationDao.add_inbound(query_db, inbound) inbound = await OperationDao.add_inbound(query_db, inbound)
await OperationDao.replace_inbound_items( await OperationDao.replace_inbound_items(
query_db, query_db,
inbound.inbound_id, inbound.inbound_id,
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time), cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time, test_scope.batch_id),
) )
await query_db.commit() await query_db.commit()
return cls._success('新增成功') return cls._success('新增成功')
@@ -393,13 +415,16 @@ class InboundService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: InboundUpdateModel, page_object: InboundUpdateModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user) _, user_name = cls._current_identity(current_user)
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
inbound = await OperationDao.get_inbound_for_update( inbound = await OperationDao.get_inbound_for_update(
query_db, query_db,
page_object.inbound_id, page_object.inbound_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if inbound is None: if inbound is None:
@@ -410,15 +435,17 @@ class InboundService:
query_db, query_db,
page_object.warehouse_id, page_object.warehouse_id,
current_user, current_user,
test_scope,
) )
ordered_ids = sorted(page_object.pallet_ids) ordered_ids = sorted(page_object.pallet_ids)
await cls._lock_and_validate_pallets(query_db, ordered_ids) await cls._lock_and_validate_pallets(query_db, ordered_ids, test_scope)
await cls._lock_and_authorize_warehouses( await cls._lock_and_authorize_warehouses(
query_db, query_db,
current_user, current_user,
warehouse_ids={inbound.warehouse_id, page_object.warehouse_id}, warehouse_ids={inbound.warehouse_id, page_object.warehouse_id},
hidden_warehouse_ids={inbound.warehouse_id}, hidden_warehouse_ids={inbound.warehouse_id},
require_active=True, require_active=True,
test_scope=test_scope,
) )
current_time = datetime.now() current_time = datetime.now()
@@ -429,7 +456,7 @@ class InboundService:
await OperationDao.replace_inbound_items( await OperationDao.replace_inbound_items(
query_db, query_db,
inbound.inbound_id, inbound.inbound_id,
cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time), cls._build_items(inbound.inbound_id, ordered_ids, user_name, current_time, test_scope.batch_id),
) )
await query_db.commit() await query_db.commit()
return cls._success('修改成功') return cls._success('修改成功')
@@ -446,47 +473,9 @@ class InboundService:
query_db: AsyncSession, query_db: AsyncSession,
inbound_ids: list[int], inbound_ids: list[int],
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: raise ServiceWarning(message='不支持单条物理删除,请通过测试数据批次清理')
if not inbound_ids or any(inbound_id <= 0 for inbound_id in inbound_ids):
raise ServiceException(message='入库单ID必须为正整数')
if len(inbound_ids) != len(set(inbound_ids)):
raise ServiceException(message='入库单ID不能重复')
ordered_ids = sorted(inbound_ids)
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
locked_inbounds = []
for inbound_id in ordered_ids:
inbound = await OperationDao.get_inbound_for_update(
query_db,
inbound_id,
warehouse_ids=warehouse_ids,
)
if inbound is None:
cls._raise_inbound_not_accessible(current_user)
if inbound.status != DocumentStatus.DRAFT.value:
raise ServiceException(message='已确认入库单不允许删除')
locked_inbounds.append(inbound)
locked_warehouse_ids = {inbound.warehouse_id for inbound in locked_inbounds}
await cls._lock_and_authorize_warehouses(
query_db,
current_user,
warehouse_ids=locked_warehouse_ids,
hidden_warehouse_ids=locked_warehouse_ids,
require_active=False,
)
for inbound_id in ordered_ids:
await OperationDao.delete_inbound(query_db, inbound_id)
await query_db.commit()
return cls._success('删除成功')
except IntegrityError:
await query_db.rollback()
raise ServiceException(message='删除入库单失败,单据状态或关联数据已变化') from None
except Exception:
await query_db.rollback()
raise
@staticmethod @staticmethod
def _is_exact_confirmed( def _is_exact_confirmed(
@@ -510,12 +499,14 @@ class InboundService:
idempotency_key: str, idempotency_key: str,
request_hash: str, request_hash: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
current = await OperationDao.get_inbound_by_id( current = await OperationDao.get_inbound_by_id(
query_db, query_db,
inbound_id, inbound_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if current is None: if current is None:
@@ -523,6 +514,7 @@ class InboundService:
recovered = await OperationDao.get_inbound_by_confirm_idempotency_key( recovered = await OperationDao.get_inbound_by_confirm_idempotency_key(
query_db, query_db,
idempotency_key, idempotency_key,
test_scope,
warehouse_ids=[current.warehouse_id], warehouse_ids=[current.warehouse_id],
) )
if recovered is None: if recovered is None:
@@ -537,6 +529,7 @@ class InboundService:
locked_recovered = await OperationDao.get_inbound_for_update( locked_recovered = await OperationDao.get_inbound_for_update(
query_db, query_db,
inbound_id, inbound_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if locked_recovered is None: if locked_recovered is None:
@@ -552,6 +545,7 @@ class InboundService:
query_db, query_db,
locked_recovered, locked_recovered,
current_user, current_user,
test_scope=test_scope,
require_active=False, require_active=False,
) )
return cls._success('确认成功') return cls._success('确认成功')
@@ -565,13 +559,16 @@ class InboundService:
inbound_id: int, inbound_id: int,
page_object: ConfirmCommandModel, page_object: ConfirmCommandModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user) user_id, user_name = cls._current_identity(current_user)
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
inbound = await OperationDao.get_inbound_by_id( inbound = await OperationDao.get_inbound_by_id(
query_db, query_db,
inbound_id, inbound_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if inbound is None: if inbound is None:
@@ -582,6 +579,7 @@ class InboundService:
existing = await OperationDao.get_inbound_by_confirm_idempotency_key( existing = await OperationDao.get_inbound_by_confirm_idempotency_key(
query_db, query_db,
idempotency_key, idempotency_key,
test_scope,
warehouse_ids=[inbound.warehouse_id], warehouse_ids=[inbound.warehouse_id],
) )
if existing is not None: if existing is not None:
@@ -601,6 +599,7 @@ class InboundService:
locked_inbound = await OperationDao.get_inbound_for_update( locked_inbound = await OperationDao.get_inbound_for_update(
query_db, query_db,
inbound_id, inbound_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if locked_inbound is None: if locked_inbound is None:
@@ -616,6 +615,7 @@ class InboundService:
query_db, query_db,
locked_inbound, locked_inbound,
current_user, current_user,
test_scope=test_scope,
require_active=False, require_active=False,
) )
await query_db.rollback() await query_db.rollback()
@@ -634,11 +634,12 @@ class InboundService:
if len(pallet_ids) != len(set(pallet_ids)): if len(pallet_ids) != len(set(pallet_ids)):
raise ServiceException(message='入库单托盘不能重复') raise ServiceException(message='入库单托盘不能重复')
ordered_ids = sorted(pallet_ids) ordered_ids = sorted(pallet_ids)
inventories, _ = await cls._lock_and_validate_pallets(query_db, ordered_ids) inventories, _ = await cls._lock_and_validate_pallets(query_db, ordered_ids, test_scope)
await cls._lock_and_reauthorize_warehouse( await cls._lock_and_reauthorize_warehouse(
query_db, query_db,
locked_inbound, locked_inbound,
current_user, current_user,
test_scope=test_scope,
) )
for inventory in inventories: for inventory in inventories:
@@ -691,6 +692,7 @@ class InboundService:
occurred_at=current_time, occurred_at=current_time,
idempotency_key=idempotency_key, idempotency_key=idempotency_key,
request_hash=request_hash, request_hash=request_hash,
test_batch_id=test_scope.batch_id,
) )
await InventoryDao.add_event(query_db, inventory, event) await InventoryDao.add_event(query_db, inventory, event)
@@ -704,6 +706,7 @@ class InboundService:
page_object.idempotency_key, page_object.idempotency_key,
cls._build_confirm_request_hash(inbound_id), cls._build_confirm_request_hash(inbound_id),
current_user, current_user,
test_scope,
) )
except Exception: except Exception:
await query_db.rollback() await query_db.rollback()
@@ -18,6 +18,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus
from module_business.domain.pallet_state_machine import PalletStateMachine from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
@@ -37,6 +38,7 @@ from module_business.entity.vo.operation_vo import (
ReturnScanQueryModel, ReturnScanQueryModel,
) )
from module_business.service.access_service import AccessService from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class ReturnService: class ReturnService:
@@ -207,6 +209,7 @@ class ReturnService:
query_db: AsyncSession, query_db: AsyncSession,
query_object: ReturnPageQueryModel, query_object: ReturnPageQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
) -> PageModel | list[ReturnModel]: ) -> PageModel | list[ReturnModel]:
site_ids, warehouse_ids = await cls._scopes(query_db, current_user) site_ids, warehouse_ids = await cls._scopes(query_db, current_user)
@@ -224,9 +227,10 @@ class ReturnService:
result = await OperationDao.get_return_list( result = await OperationDao.get_return_list(
query_db, query_db,
query_object, query_object,
is_page,
site_ids=site_ids, site_ids=site_ids,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
scope=test_scope,
is_page=is_page,
) )
rows = result.rows if isinstance(result, PageModel) else result rows = result.rows if isinstance(result, PageModel) else result
models = [ReturnModel.model_validate(record) for record in rows] models = [ReturnModel.model_validate(record) for record in rows]
@@ -240,11 +244,13 @@ class ReturnService:
query_db: AsyncSession, query_db: AsyncSession,
return_id: int, return_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> ReturnModel: ) -> ReturnModel:
site_ids, warehouse_ids = await cls._scopes(query_db, current_user) site_ids, warehouse_ids = await cls._scopes(query_db, current_user)
return_order = await OperationDao.get_return_by_id( return_order = await OperationDao.get_return_by_id(
query_db, query_db,
return_id, return_id,
test_scope,
site_ids=site_ids, site_ids=site_ids,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
@@ -257,11 +263,13 @@ class ReturnService:
cls, cls,
query_db: AsyncSession, query_db: AsyncSession,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]: ) -> list[BusinessOptionModel]:
cls._current_identity(current_user) cls._current_identity(current_user)
warehouses = await MasterDao.list_warehouse( warehouses = await MasterDao.list_warehouse(
query_db, query_db,
WarehousePageQueryModel(status='0'), WarehousePageQueryModel(status='0'),
test_scope,
False, False,
) )
options = [ options = [
@@ -333,11 +341,12 @@ class ReturnService:
query_db: AsyncSession, query_db: AsyncSession,
qr_token: str, qr_token: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel: ) -> PalletScanResponseModel:
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token) pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None: if pallet is None:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id) inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None or inventory.site_id is None: if inventory is None or inventory.site_id is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在或不在客户现场') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在或不在客户现场')
await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id) await cls._assert_pallet_site_access(query_db, current_user, inventory.site_id)
@@ -345,7 +354,7 @@ class ReturnService:
filling = None filling = None
if inventory.active_filling_id is not None: if inventory.active_filling_id is not None:
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id) filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None: if filling is None:
raise ServiceException(message='托盘活动装填周期不存在') raise ServiceException(message='托盘活动装填周期不存在')
cls._validate_active_filling(filling, inventory) cls._validate_active_filling(filling, inventory)
@@ -358,12 +367,14 @@ class ReturnService:
qr_token: str, qr_token: str,
return_id: int, return_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel: ) -> PalletScanResponseModel:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
confirm_site_ids = None if warehouse_ids is None else [] confirm_site_ids = None if warehouse_ids is None else []
return_order = await OperationDao.get_return_by_id( return_order = await OperationDao.get_return_by_id(
query_db, query_db,
return_id, return_id,
test_scope,
site_ids=confirm_site_ids, site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
@@ -372,13 +383,13 @@ class ReturnService:
if return_order.status != ReturnStatus.REQUESTED.value: if return_order.status != ReturnStatus.REQUESTED.value:
raise ServiceException(message='退回单当前状态不允许确认扫码') raise ServiceException(message='退回单当前状态不允许确认扫码')
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token) pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None: if pallet is None:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
matching_items = [item for item in return_order.items if item.pallet_id == pallet.pallet_id] matching_items = [item for item in return_order.items if item.pallet_id == pallet.pallet_id]
if len(matching_items) != 1: if len(matching_items) != 1:
cls._raise_pallet_not_accessible(current_user, '扫码托盘不属于该退回单') cls._raise_pallet_not_accessible(current_user, '扫码托盘不属于该退回单')
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id) inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None: if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
item = matching_items[0] item = matching_items[0]
@@ -386,7 +397,7 @@ class ReturnService:
filling = None filling = None
if inventory.active_filling_id is not None: if inventory.active_filling_id is not None:
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id) filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None: if filling is None:
raise ServiceException(message='托盘活动装填周期不存在') raise ServiceException(message='托盘活动装填周期不存在')
cls._validate_active_filling(filling, inventory) cls._validate_active_filling(filling, inventory)
@@ -424,12 +435,13 @@ class ReturnService:
qr_token: str, qr_token: str,
query_object: ReturnScanQueryModel, query_object: ReturnScanQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel: ) -> PalletScanResponseModel:
normalized_qr = qr_token.strip() normalized_qr = qr_token.strip()
if not normalized_qr: if not normalized_qr:
raise ServiceException(message='托盘二维码不能为空') raise ServiceException(message='托盘二维码不能为空')
if query_object.mode == 'request': if query_object.mode == 'request':
return await cls._scan_request_pallet(query_db, normalized_qr, current_user) return await cls._scan_request_pallet(query_db, normalized_qr, current_user, test_scope)
if query_object.return_id is None: if query_object.return_id is None:
raise ServiceException(message='确认退回扫码时必须提供退回单ID') raise ServiceException(message='确认退回扫码时必须提供退回单ID')
return await cls._scan_confirm_pallet( return await cls._scan_confirm_pallet(
@@ -437,6 +449,7 @@ class ReturnService:
normalized_qr, normalized_qr,
query_object.return_id, query_object.return_id,
current_user, current_user,
test_scope,
) )
@classmethod @classmethod
@@ -447,6 +460,7 @@ class ReturnService:
source_site_id: int, source_site_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
inventories: list[BizPalletInventory], inventories: list[BizPalletInventory],
test_scope: TestBatchScope,
) -> tuple[ ) -> tuple[
list[BizPalletInventory], list[BizPalletInventory],
list[BizPallet], list[BizPallet],
@@ -466,7 +480,7 @@ class ReturnService:
): ):
raise ServiceException(message='托盘当前状态不允许发起退回') raise ServiceException(message='托盘当前状态不允许发起退回')
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids) pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets} pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
if set(pallet_by_id) != set(ordered_ids): if set(pallet_by_id) != set(ordered_ids):
cls._raise_pallet_not_accessible(current_user, '存在不存在或库存缺失的托盘') cls._raise_pallet_not_accessible(current_user, '存在不存在或库存缺失的托盘')
@@ -481,7 +495,9 @@ class ReturnService:
} }
) )
for filling_id in filling_ids: for filling_id in filling_ids:
filling_by_id[filling_id] = await OperationDao.get_active_filling_for_update(query_db, filling_id) filling_by_id[filling_id] = await OperationDao.get_active_filling_for_update(
query_db, filling_id, test_scope
)
return ordered_inventories, ordered_pallets, filling_by_id return ordered_inventories, ordered_pallets, filling_by_id
@classmethod @classmethod
@@ -513,6 +529,7 @@ class ReturnService:
inventories: list[BizPalletInventory], inventories: list[BizPalletInventory],
user_name: str, user_name: str,
current_time: datetime, current_time: datetime,
test_batch_id: int | None,
) -> list[BizReturnItem]: ) -> list[BizReturnItem]:
return [ return [
BizReturnItem( BizReturnItem(
@@ -522,6 +539,7 @@ class ReturnService:
remaining_packages_snapshot=inventory.remaining_packages, remaining_packages_snapshot=inventory.remaining_packages,
create_by=user_name, create_by=user_name,
create_time=current_time, create_time=current_time,
test_batch_id=test_batch_id,
) )
for inventory in inventories for inventory in inventories
] ]
@@ -532,10 +550,12 @@ class ReturnService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: ReturnRequestModel, page_object: ReturnRequestModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> None: ) -> None:
warehouse = await MasterDao.get_warehouse( warehouse = await MasterDao.get_warehouse(
query_db, query_db,
page_object.target_warehouse_id, page_object.target_warehouse_id,
test_scope,
for_update=True, for_update=True,
) )
if warehouse is None: if warehouse is None:
@@ -543,7 +563,7 @@ class ReturnService:
if warehouse.status != '0': if warehouse.status != '0':
raise ServiceException(message='目标仓库已停用') raise ServiceException(message='目标仓库已停用')
site = await MasterDao.get_site(query_db, page_object.source_site_id, for_update=True) site = await MasterDao.get_site(query_db, page_object.source_site_id, test_scope, for_update=True)
await AccessService.assert_site_access( await AccessService.assert_site_access(
query_db, query_db,
current_user, current_user,
@@ -561,9 +581,11 @@ class ReturnService:
query_db: AsyncSession, query_db: AsyncSession,
return_order: BizReturnOrder, return_order: BizReturnOrder,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> None: ) -> None:
site = await MasterDao.get_site(query_db, return_order.source_site_id, for_update=True) site = await MasterDao.get_site(query_db, return_order.source_site_id, test_scope, for_update=True)
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
await AccessService.assert_site_access( await AccessService.assert_site_access(
query_db, query_db,
current_user, current_user,
@@ -582,6 +604,7 @@ class ReturnService:
page_object: ReturnRequestModel, page_object: ReturnRequestModel,
request_hash: str, request_hash: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
site_ids = await cls._site_scope(query_db, current_user) site_ids = await cls._site_scope(query_db, current_user)
@@ -593,12 +616,14 @@ class ReturnService:
page_object.idempotency_key, page_object.idempotency_key,
source_site_id=page_object.source_site_id, source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id, target_warehouse_id=page_object.target_warehouse_id,
scope=test_scope,
) )
if recovered is None or not cls._is_exact_request_result(recovered, page_object, request_hash): if recovered is None or not cls._is_exact_request_result(recovered, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等冲突') raise ServiceException(message='申请退回失败,幂等冲突')
locked = await OperationDao.get_return_for_update( locked = await OperationDao.get_return_for_update(
query_db, query_db,
recovered.return_id, recovered.return_id,
test_scope,
site_ids=site_ids, site_ids=site_ids,
warehouse_ids=request_warehouse_ids, warehouse_ids=request_warehouse_ids,
) )
@@ -611,7 +636,7 @@ class ReturnService:
require_items=True, require_items=True,
): ):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用') raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
await cls._lock_and_reauthorize_request_site(query_db, locked, current_user) await cls._lock_and_reauthorize_request_site(query_db, locked, current_user, test_scope)
return cls._success('申请成功', locked.return_id) return cls._success('申请成功', locked.return_id)
finally: finally:
await query_db.rollback() await query_db.rollback()
@@ -622,8 +647,10 @@ class ReturnService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: ReturnRequestModel, page_object: ReturnRequestModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user) user_id, user_name = cls._current_identity(current_user)
ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids) ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids)
request_hash = cls._build_request_hash(page_object) request_hash = cls._build_request_hash(page_object)
@@ -644,6 +671,7 @@ class ReturnService:
page_object.idempotency_key, page_object.idempotency_key,
source_site_id=page_object.source_site_id, source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id, target_warehouse_id=page_object.target_warehouse_id,
scope=test_scope,
) )
if existing is not None and not cls._is_exact_request_result(existing, page_object, request_hash): if existing is not None and not cls._is_exact_request_result(existing, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用') raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
@@ -654,6 +682,7 @@ class ReturnService:
locked_existing = await OperationDao.get_return_for_update( locked_existing = await OperationDao.get_return_for_update(
query_db, query_db,
preflight_existing_id, preflight_existing_id,
test_scope,
site_ids=preflight_site_ids, site_ids=preflight_site_ids,
warehouse_ids=request_warehouse_ids, warehouse_ids=request_warehouse_ids,
) )
@@ -666,12 +695,12 @@ class ReturnService:
require_items=True, require_items=True,
): ):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用') raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
await cls._lock_and_reauthorize_request_site(query_db, locked_existing, current_user) await cls._lock_and_reauthorize_request_site(query_db, locked_existing, current_user, test_scope)
return_id = locked_existing.return_id return_id = locked_existing.return_id
await query_db.rollback() await query_db.rollback()
return cls._success('申请成功', return_id) return cls._success('申请成功', return_id)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids) inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
try: try:
await AccessService.assert_site_access( await AccessService.assert_site_access(
query_db, query_db,
@@ -688,12 +717,13 @@ class ReturnService:
page_object.idempotency_key, page_object.idempotency_key,
source_site_id=page_object.source_site_id, source_site_id=page_object.source_site_id,
target_warehouse_id=page_object.target_warehouse_id, target_warehouse_id=page_object.target_warehouse_id,
scope=test_scope,
) )
if fresh_existing is not None: if fresh_existing is not None:
if not cls._is_exact_request_result(fresh_existing, page_object, request_hash): if not cls._is_exact_request_result(fresh_existing, page_object, request_hash):
raise ServiceException(message='申请退回失败,幂等键已被其他请求使用') raise ServiceException(message='申请退回失败,幂等键已被其他请求使用')
fresh_existing_id = int(fresh_existing.return_id) fresh_existing_id = int(fresh_existing.return_id)
await cls._lock_and_reauthorize_request_site(query_db, fresh_existing, current_user) await cls._lock_and_reauthorize_request_site(query_db, fresh_existing, current_user, test_scope)
await query_db.rollback() await query_db.rollback()
return cls._success('申请成功', fresh_existing_id) return cls._success('申请成功', fresh_existing_id)
@@ -703,8 +733,9 @@ class ReturnService:
page_object.source_site_id, page_object.source_site_id,
current_user, current_user,
inventories, inventories,
test_scope,
) )
await cls._lock_new_request_route(query_db, page_object, current_user) await cls._lock_new_request_route(query_db, page_object, current_user, test_scope)
cls._validate_locked_request_pallets( cls._validate_locked_request_pallets(
inventories, inventories,
pallets, pallets,
@@ -732,12 +763,13 @@ class ReturnService:
create_time=current_time, create_time=current_time,
update_by=user_name, update_by=user_name,
update_time=current_time, update_time=current_time,
test_batch_id=test_scope.batch_id,
) )
return_order = await OperationDao.add_return_order(query_db, return_order) return_order = await OperationDao.add_return_order(query_db, return_order)
return_id = int(return_order.return_id) return_id = int(return_order.return_id)
await OperationDao.add_return_items( await OperationDao.add_return_items(
query_db, query_db,
cls._build_return_items(return_id, inventories, user_name, current_time), cls._build_return_items(return_id, inventories, user_name, current_time, test_scope.batch_id),
) )
for inventory in inventories: for inventory in inventories:
@@ -768,6 +800,7 @@ class ReturnService:
occurred_at=current_time, occurred_at=current_time,
idempotency_key=page_object.idempotency_key, idempotency_key=page_object.idempotency_key,
request_hash=request_hash, request_hash=request_hash,
test_batch_id=test_scope.batch_id,
) )
await InventoryDao.add_event(query_db, inventory, event) await InventoryDao.add_event(query_db, inventory, event)
@@ -825,10 +858,11 @@ class ReturnService:
query_db: AsyncSession, query_db: AsyncSession,
return_order: BizReturnOrder, return_order: BizReturnOrder,
items: list[BizReturnItem], items: list[BizReturnItem],
test_scope: TestBatchScope,
) -> tuple[list[BizPalletInventory], dict[int, BizFillingRecord]]: ) -> tuple[list[BizPalletInventory], dict[int, BizFillingRecord]]:
ordered_ids = sorted(item.pallet_id for item in items) ordered_ids = sorted(item.pallet_id for item in items)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids) inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids) pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories} inventory_by_id = {inventory.pallet_id: inventory for inventory in inventories}
pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets} pallet_by_id = {pallet.pallet_id: pallet for pallet in pallets}
item_by_id = {item.pallet_id: item for item in items} item_by_id = {item.pallet_id: item for item in items}
@@ -857,7 +891,7 @@ class ReturnService:
} }
) )
for filling_id in filling_ids: for filling_id in filling_ids:
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id) filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
if filling is None: if filling is None:
raise ServiceException(message='托盘活动装填周期不存在或已关闭') raise ServiceException(message='托盘活动装填周期不存在或已关闭')
filling_by_id[filling_id] = filling filling_by_id[filling_id] = filling
@@ -873,11 +907,13 @@ class ReturnService:
return_order: BizReturnOrder, return_order: BizReturnOrder,
current_user: CurrentUserModel, current_user: CurrentUserModel,
*, *,
test_scope: TestBatchScope,
require_active: bool, require_active: bool,
) -> None: ) -> None:
warehouse = await MasterDao.get_warehouse( warehouse = await MasterDao.get_warehouse(
query_db, query_db,
return_order.target_warehouse_id, return_order.target_warehouse_id,
test_scope,
for_update=True, for_update=True,
) )
try: try:
@@ -902,6 +938,7 @@ class ReturnService:
page_object: ReturnConfirmModel, page_object: ReturnConfirmModel,
request_hash: str, request_hash: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
warehouse_ids = await cls._warehouse_scope(query_db, current_user) warehouse_ids = await cls._warehouse_scope(query_db, current_user)
@@ -909,6 +946,7 @@ class ReturnService:
route = await OperationDao.get_return_by_id( route = await OperationDao.get_return_by_id(
query_db, query_db,
return_id, return_id,
test_scope,
site_ids=confirm_site_ids, site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
@@ -919,6 +957,7 @@ class ReturnService:
page_object.idempotency_key, page_object.idempotency_key,
source_site_id=route.source_site_id, source_site_id=route.source_site_id,
target_warehouse_id=route.target_warehouse_id, target_warehouse_id=route.target_warehouse_id,
scope=test_scope,
) )
if recovered is None: if recovered is None:
raise ServiceException(message='确认退回失败,幂等冲突') raise ServiceException(message='确认退回失败,幂等冲突')
@@ -927,6 +966,7 @@ class ReturnService:
locked = await OperationDao.get_return_for_update( locked = await OperationDao.get_return_for_update(
query_db, query_db,
return_id, return_id,
test_scope,
site_ids=confirm_site_ids, site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
@@ -944,6 +984,7 @@ class ReturnService:
query_db, query_db,
locked, locked,
current_user, current_user,
test_scope=test_scope,
require_active=False, require_active=False,
) )
return cls._success('确认成功', return_id) return cls._success('确认成功', return_id)
@@ -957,8 +998,10 @@ class ReturnService:
return_id: int, return_id: int,
page_object: ReturnConfirmModel, page_object: ReturnConfirmModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user) user_id, user_name = cls._current_identity(current_user)
scanned_ids = cls._validate_pallet_ids(page_object.scanned_pallet_ids) scanned_ids = cls._validate_pallet_ids(page_object.scanned_pallet_ids)
request_hash = cls._build_confirm_request_hash(return_id, page_object) request_hash = cls._build_confirm_request_hash(return_id, page_object)
@@ -967,6 +1010,7 @@ class ReturnService:
return_order = await OperationDao.get_return_by_id( return_order = await OperationDao.get_return_by_id(
query_db, query_db,
return_id, return_id,
test_scope,
site_ids=confirm_site_ids, site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
@@ -985,6 +1029,7 @@ class ReturnService:
page_object.idempotency_key, page_object.idempotency_key,
source_site_id=return_order.source_site_id, source_site_id=return_order.source_site_id,
target_warehouse_id=return_order.target_warehouse_id, target_warehouse_id=return_order.target_warehouse_id,
scope=test_scope,
) )
if existing is not None and not cls._is_exact_confirm_result( if existing is not None and not cls._is_exact_confirm_result(
existing, existing,
@@ -998,6 +1043,7 @@ class ReturnService:
locked = await OperationDao.get_return_for_update( locked = await OperationDao.get_return_for_update(
query_db, query_db,
return_id, return_id,
test_scope,
site_ids=confirm_site_ids, site_ids=confirm_site_ids,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
@@ -1015,6 +1061,7 @@ class ReturnService:
query_db, query_db,
locked, locked,
current_user, current_user,
test_scope=test_scope,
require_active=False, require_active=False,
) )
await query_db.rollback() await query_db.rollback()
@@ -1034,11 +1081,12 @@ class ReturnService:
if scanned_ids != sorted(item_ids): if scanned_ids != sorted(item_ids):
raise ServiceException(message='确认扫码托盘集合与退回单明细不一致') raise ServiceException(message='确认扫码托盘集合与退回单明细不一致')
inventories, _ = await cls._lock_confirm_pallets(query_db, locked, items) inventories, _ = await cls._lock_confirm_pallets(query_db, locked, items, test_scope)
await cls._lock_and_reauthorize_confirm_warehouse( await cls._lock_and_reauthorize_confirm_warehouse(
query_db, query_db,
locked, locked,
current_user, current_user,
test_scope=test_scope,
require_active=True, require_active=True,
) )
for inventory in inventories: for inventory in inventories:
@@ -1096,6 +1144,7 @@ class ReturnService:
occurred_at=current_time, occurred_at=current_time,
idempotency_key=page_object.idempotency_key, idempotency_key=page_object.idempotency_key,
request_hash=request_hash, request_hash=request_hash,
test_batch_id=test_scope.batch_id,
) )
await InventoryDao.add_event(query_db, inventory, event) await InventoryDao.add_event(query_db, inventory, event)
@@ -1109,6 +1158,7 @@ class ReturnService:
page_object, page_object,
cls._build_confirm_request_hash(return_id, page_object), cls._build_confirm_request_hash(return_id, page_object),
current_user, current_user,
test_scope,
) )
except Exception: except Exception:
await query_db.rollback() await query_db.rollback()
@@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import CrudResponseModel, PageModel from common.vo import CrudResponseModel, PageModel
from exceptions.exception import PermissionException, ServiceException from exceptions.exception import PermissionException, ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
from module_business.dao.access_dao import AccessDao from module_business.dao.access_dao import AccessDao
from module_business.dao.inventory_dao import InventoryDao from module_business.dao.inventory_dao import InventoryDao
@@ -16,6 +16,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
@@ -36,6 +37,7 @@ from module_business.entity.vo.operation_vo import (
ShipmentUpdateModel, ShipmentUpdateModel,
) )
from module_business.service.access_service import AccessService from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class ShipmentService: class ShipmentService:
@@ -97,15 +99,16 @@ class ShipmentService:
source_warehouse_id: int, source_warehouse_id: int,
target_site_id: int, target_site_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> tuple[Any, Any]: ) -> tuple[Any, Any]:
await AccessService.assert_warehouse_access(query_db, current_user, source_warehouse_id) await AccessService.assert_warehouse_access(query_db, current_user, source_warehouse_id)
warehouse = await MasterDao.get_warehouse(query_db, source_warehouse_id) warehouse = await MasterDao.get_warehouse(query_db, source_warehouse_id, test_scope)
if warehouse is None: if warehouse is None:
raise ServiceException(message='来源仓库不存在') raise ServiceException(message='来源仓库不存在')
if warehouse.status != '0': if warehouse.status != '0':
raise ServiceException(message='来源仓库已停用') raise ServiceException(message='来源仓库已停用')
await AccessService.assert_site_access(query_db, current_user, target_site_id) await AccessService.assert_site_access(query_db, current_user, target_site_id)
site = await MasterDao.get_site(query_db, target_site_id) site = await MasterDao.get_site(query_db, target_site_id, test_scope)
if site is None: if site is None:
raise ServiceException(message='目标现场不存在') raise ServiceException(message='目标现场不存在')
if site.status != '0': if site.status != '0':
@@ -158,10 +161,11 @@ class ShipmentService:
query_db: AsyncSession, query_db: AsyncSession,
pallet_ids: list[int], pallet_ids: list[int],
source_warehouse_id: int, source_warehouse_id: int,
test_scope: TestBatchScope,
) -> tuple[list[BizPalletInventory], list[BizPallet], list[BizFillingRecord]]: ) -> tuple[list[BizPalletInventory], list[BizPallet], list[BizFillingRecord]]:
ordered_ids = cls._validate_pallet_ids(pallet_ids) ordered_ids = cls._validate_pallet_ids(pallet_ids)
inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids) inventories = await InventoryDao.get_inventories_for_update(query_db, ordered_ids, test_scope)
pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids) pallets = await InventoryDao.get_pallets_by_ids(query_db, ordered_ids, test_scope)
if ( if (
len(inventories) != len(ordered_ids) len(inventories) != len(ordered_ids)
or len(pallets) != len(ordered_ids) or len(pallets) != len(ordered_ids)
@@ -181,7 +185,7 @@ class ShipmentService:
fillings = [] fillings = []
for filling_id in sorted(set(active_filling_ids)): for filling_id in sorted(set(active_filling_ids)):
filling = await OperationDao.get_active_filling_for_update(query_db, filling_id) filling = await OperationDao.get_active_filling_for_update(query_db, filling_id, test_scope)
if filling is None: if filling is None:
raise ServiceException(message='托盘活动装填周期不存在或已关闭') raise ServiceException(message='托盘活动装填周期不存在或已关闭')
fillings.append(filling) fillings.append(filling)
@@ -207,6 +211,7 @@ class ShipmentService:
pallet_ids: list[int], pallet_ids: list[int],
user_name: str, user_name: str,
current_time: datetime, current_time: datetime,
test_batch_id: int | None,
) -> list[BizShipmentItem]: ) -> list[BizShipmentItem]:
return [ return [
BizShipmentItem( BizShipmentItem(
@@ -216,6 +221,7 @@ class ShipmentService:
package_qty=None, package_qty=None,
create_by=user_name, create_by=user_name,
create_time=current_time, create_time=current_time,
test_batch_id=test_batch_id,
) )
for pallet_id in sorted(pallet_ids) for pallet_id in sorted(pallet_ids)
] ]
@@ -239,6 +245,7 @@ class ShipmentService:
query_db: AsyncSession, query_db: AsyncSession,
query_object: ShipmentPageQueryModel, query_object: ShipmentPageQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
) -> PageModel | list[ShipmentModel]: ) -> PageModel | list[ShipmentModel]:
warehouse_ids, site_ids = await cls._scopes(query_db, current_user) warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
@@ -268,9 +275,10 @@ class ShipmentService:
result = await OperationDao.get_shipment_list( result = await OperationDao.get_shipment_list(
query_db, query_db,
query_object, query_object,
is_page,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
site_ids=site_ids, site_ids=site_ids,
scope=test_scope,
is_page=is_page,
) )
rows = result.rows if isinstance(result, PageModel) else result rows = result.rows if isinstance(result, PageModel) else result
models = [ShipmentModel.model_validate(record) for record in rows] models = [ShipmentModel.model_validate(record) for record in rows]
@@ -284,11 +292,13 @@ class ShipmentService:
query_db: AsyncSession, query_db: AsyncSession,
shipment_id: int, shipment_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> ShipmentModel: ) -> ShipmentModel:
warehouse_ids, site_ids = await cls._scopes(query_db, current_user) warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
shipment = await OperationDao.get_shipment_by_id( shipment = await OperationDao.get_shipment_by_id(
query_db, query_db,
shipment_id, shipment_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
site_ids=site_ids, site_ids=site_ids,
) )
@@ -301,12 +311,14 @@ class ShipmentService:
cls, cls,
query_db: AsyncSession, query_db: AsyncSession,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]: ) -> list[BusinessOptionModel]:
warehouse_ids, _ = await cls._scopes(query_db, current_user) warehouse_ids, _ = await cls._scopes(query_db, current_user)
allowed_ids = None if warehouse_ids is None else set(warehouse_ids) allowed_ids = None if warehouse_ids is None else set(warehouse_ids)
warehouses = await MasterDao.list_warehouse( warehouses = await MasterDao.list_warehouse(
query_db, query_db,
WarehousePageQueryModel(status='0'), WarehousePageQueryModel(status='0'),
test_scope,
False, False,
) )
options = [] options = []
@@ -330,12 +342,14 @@ class ShipmentService:
cls, cls,
query_db: AsyncSession, query_db: AsyncSession,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]: ) -> list[BusinessOptionModel]:
_, site_ids = await cls._scopes(query_db, current_user) _, site_ids = await cls._scopes(query_db, current_user)
allowed_ids = None if site_ids is None else set(site_ids) allowed_ids = None if site_ids is None else set(site_ids)
sites = await MasterDao.list_site( sites = await MasterDao.list_site(
query_db, query_db,
SitePageQueryModel(status='0'), SitePageQueryModel(status='0'),
test_scope,
False, False,
) )
options = [] options = []
@@ -388,13 +402,14 @@ class ShipmentService:
qr_token: str, qr_token: str,
query_object: ShipmentScanQueryModel, query_object: ShipmentScanQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel: ) -> PalletScanResponseModel:
await AccessService.assert_warehouse_access( await AccessService.assert_warehouse_access(
query_db, query_db,
current_user, current_user,
query_object.source_warehouse_id, query_object.source_warehouse_id,
) )
warehouse = await MasterDao.get_warehouse(query_db, query_object.source_warehouse_id) warehouse = await MasterDao.get_warehouse(query_db, query_object.source_warehouse_id, test_scope)
if warehouse is None: if warehouse is None:
raise ServiceException(message='来源仓库不存在') raise ServiceException(message='来源仓库不存在')
if warehouse.status != '0': if warehouse.status != '0':
@@ -404,16 +419,16 @@ class ShipmentService:
current_user, current_user,
query_object.target_site_id, query_object.target_site_id,
) )
site = await MasterDao.get_site(query_db, query_object.target_site_id) site = await MasterDao.get_site(query_db, query_object.target_site_id, test_scope)
if site is None: if site is None:
raise ServiceException(message='目标现场不存在') raise ServiceException(message='目标现场不存在')
if site.status != '0': if site.status != '0':
raise ServiceException(message='目标现场已停用') raise ServiceException(message='目标现场已停用')
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token) pallet = await InventoryDao.get_pallet_by_qr_token(query_db, qr_token, test_scope)
if pallet is None: if pallet is None:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id) inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None: if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.pallet_id != pallet.pallet_id or inventory.warehouse_id != query_object.source_warehouse_id: if inventory.pallet_id != pallet.pallet_id or inventory.warehouse_id != query_object.source_warehouse_id:
@@ -422,7 +437,7 @@ class ShipmentService:
cls._raise_pallet_not_accessible(current_user, '托盘已在客户现场') cls._raise_pallet_not_accessible(current_user, '托盘已在客户现场')
if inventory.active_filling_id is None: if inventory.active_filling_id is None:
raise ServiceException(message='托盘不存在活动装填周期') raise ServiceException(message='托盘不存在活动装填周期')
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id) filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None: if filling is None:
raise ServiceException(message='托盘活动装填周期不存在') raise ServiceException(message='托盘活动装填周期不存在')
cls._validate_scan_pallet( cls._validate_scan_pallet(
@@ -455,20 +470,24 @@ class ShipmentService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: ShipmentCreateModel, page_object: ShipmentCreateModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user) _, user_name = cls._current_identity(current_user)
await cls._validate_active_route( await cls._validate_active_route(
query_db, query_db,
page_object.source_warehouse_id, page_object.source_warehouse_id,
page_object.target_site_id, page_object.target_site_id,
current_user, current_user,
test_scope,
) )
ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids) ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids)
await cls._lock_and_validate_pallets( await cls._lock_and_validate_pallets(
query_db, query_db,
ordered_ids, ordered_ids,
page_object.source_warehouse_id, page_object.source_warehouse_id,
test_scope,
) )
await cls._lock_and_authorize_routes( await cls._lock_and_authorize_routes(
query_db, query_db,
@@ -478,6 +497,7 @@ class ShipmentService:
hidden_warehouse_ids=set(), hidden_warehouse_ids=set(),
hidden_site_ids=set(), hidden_site_ids=set(),
require_active=True, require_active=True,
test_scope=test_scope,
) )
current_time = datetime.now() current_time = datetime.now()
@@ -490,12 +510,13 @@ class ShipmentService:
create_time=current_time, create_time=current_time,
update_by=user_name, update_by=user_name,
update_time=current_time, update_time=current_time,
test_batch_id=test_scope.batch_id,
) )
shipment = await OperationDao.add_shipment(query_db, shipment) shipment = await OperationDao.add_shipment(query_db, shipment)
await OperationDao.replace_shipment_items( await OperationDao.replace_shipment_items(
query_db, query_db,
shipment.shipment_id, shipment.shipment_id,
cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time), cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time, test_scope.batch_id),
) )
await query_db.commit() await query_db.commit()
return cls._success('新增成功') return cls._success('新增成功')
@@ -512,13 +533,16 @@ class ShipmentService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: ShipmentUpdateModel, page_object: ShipmentUpdateModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
_, user_name = cls._current_identity(current_user) _, user_name = cls._current_identity(current_user)
warehouse_ids, site_ids = await cls._scopes(query_db, current_user) warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
shipment = await OperationDao.get_shipment_for_update( shipment = await OperationDao.get_shipment_for_update(
query_db, query_db,
page_object.shipment_id, page_object.shipment_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
site_ids=site_ids, site_ids=site_ids,
) )
@@ -534,12 +558,14 @@ class ShipmentService:
page_object.source_warehouse_id, page_object.source_warehouse_id,
page_object.target_site_id, page_object.target_site_id,
current_user, current_user,
test_scope,
) )
ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids) ordered_ids = cls._validate_pallet_ids(page_object.pallet_ids)
await cls._lock_and_validate_pallets( await cls._lock_and_validate_pallets(
query_db, query_db,
ordered_ids, ordered_ids,
page_object.source_warehouse_id, page_object.source_warehouse_id,
test_scope,
) )
await cls._lock_and_authorize_routes( await cls._lock_and_authorize_routes(
query_db, query_db,
@@ -555,6 +581,7 @@ class ShipmentService:
hidden_warehouse_ids={shipment.source_warehouse_id}, hidden_warehouse_ids={shipment.source_warehouse_id},
hidden_site_ids={shipment.target_site_id}, hidden_site_ids={shipment.target_site_id},
require_active=True, require_active=True,
test_scope=test_scope,
) )
current_time = datetime.now() current_time = datetime.now()
@@ -566,7 +593,7 @@ class ShipmentService:
await OperationDao.replace_shipment_items( await OperationDao.replace_shipment_items(
query_db, query_db,
shipment.shipment_id, shipment.shipment_id,
cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time), cls._build_items(shipment.shipment_id, ordered_ids, user_name, current_time, test_scope.batch_id),
) )
await query_db.commit() await query_db.commit()
return cls._success('修改成功') return cls._success('修改成功')
@@ -583,53 +610,9 @@ class ShipmentService:
query_db: AsyncSession, query_db: AsyncSession,
shipment_ids: list[int], shipment_ids: list[int],
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: raise ServiceWarning(message='不支持单条物理删除,请通过测试数据批次清理')
if not shipment_ids or any(
type(shipment_id) is not int or shipment_id <= 0 for shipment_id in shipment_ids
):
raise ServiceException(message='发货单ID必须为正整数')
if len(shipment_ids) != len(set(shipment_ids)):
raise ServiceException(message='发货单ID不能重复')
ordered_ids = sorted(shipment_ids)
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
locked_shipments = []
for shipment_id in ordered_ids:
shipment = await OperationDao.get_shipment_for_update(
query_db,
shipment_id,
warehouse_ids=warehouse_ids,
site_ids=site_ids,
)
if shipment is None:
cls._raise_shipment_not_accessible(current_user)
if shipment.status != DocumentStatus.DRAFT.value:
raise ServiceException(message='已确认发货单不允许删除')
locked_shipments.append(shipment)
route_warehouse_ids = {shipment.source_warehouse_id for shipment in locked_shipments}
route_site_ids = {shipment.target_site_id for shipment in locked_shipments}
await cls._lock_and_authorize_routes(
query_db,
current_user,
warehouse_ids=route_warehouse_ids,
site_ids=route_site_ids,
hidden_warehouse_ids=route_warehouse_ids,
hidden_site_ids=route_site_ids,
require_active=False,
)
for shipment_id in ordered_ids:
await OperationDao.delete_shipment(query_db, shipment_id)
await query_db.commit()
return cls._success('删除成功')
except IntegrityError:
await query_db.rollback()
raise ServiceException(message='删除发货单失败,单据状态或关联数据已变化') from None
except Exception:
await query_db.rollback()
raise
@staticmethod @staticmethod
def _is_exact_confirmed( def _is_exact_confirmed(
@@ -656,6 +639,7 @@ class ShipmentService:
hidden_warehouse_ids: set[int], hidden_warehouse_ids: set[int],
hidden_site_ids: set[int], hidden_site_ids: set[int],
require_active: bool, require_active: bool,
test_scope: TestBatchScope,
) -> None: ) -> None:
warehouses: dict[int, Any] = {} warehouses: dict[int, Any] = {}
warehouse_permission_errors: dict[int, PermissionException] = {} warehouse_permission_errors: dict[int, PermissionException] = {}
@@ -663,6 +647,7 @@ class ShipmentService:
warehouse = await MasterDao.get_warehouse( warehouse = await MasterDao.get_warehouse(
query_db, query_db,
warehouse_id, warehouse_id,
test_scope,
for_update=True, for_update=True,
) )
warehouses[warehouse_id] = warehouse warehouses[warehouse_id] = warehouse
@@ -682,6 +667,7 @@ class ShipmentService:
site = await MasterDao.get_site( site = await MasterDao.get_site(
query_db, query_db,
site_id, site_id,
test_scope,
for_update=True, for_update=True,
) )
sites[site_id] = site sites[site_id] = site
@@ -727,6 +713,7 @@ class ShipmentService:
shipment: BizShipmentOrder, shipment: BizShipmentOrder,
current_user: CurrentUserModel, current_user: CurrentUserModel,
*, *,
test_scope: TestBatchScope,
require_active: bool = True, require_active: bool = True,
) -> None: ) -> None:
await cls._lock_and_authorize_routes( await cls._lock_and_authorize_routes(
@@ -737,6 +724,7 @@ class ShipmentService:
hidden_warehouse_ids={shipment.source_warehouse_id}, hidden_warehouse_ids={shipment.source_warehouse_id},
hidden_site_ids={shipment.target_site_id}, hidden_site_ids={shipment.target_site_id},
require_active=require_active, require_active=require_active,
test_scope=test_scope,
) )
@classmethod @classmethod
@@ -747,12 +735,14 @@ class ShipmentService:
idempotency_key: str, idempotency_key: str,
request_hash: str, request_hash: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
warehouse_ids, site_ids = await cls._scopes(query_db, current_user) warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
current = await OperationDao.get_shipment_by_id( current = await OperationDao.get_shipment_by_id(
query_db, query_db,
shipment_id, shipment_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
site_ids=site_ids, site_ids=site_ids,
) )
@@ -761,6 +751,7 @@ class ShipmentService:
recovered = await OperationDao.get_shipment_by_confirm_idempotency_key( recovered = await OperationDao.get_shipment_by_confirm_idempotency_key(
query_db, query_db,
idempotency_key, idempotency_key,
test_scope,
warehouse_ids=[current.source_warehouse_id], warehouse_ids=[current.source_warehouse_id],
site_ids=[current.target_site_id], site_ids=[current.target_site_id],
) )
@@ -776,6 +767,7 @@ class ShipmentService:
locked_recovered = await OperationDao.get_shipment_for_update( locked_recovered = await OperationDao.get_shipment_for_update(
query_db, query_db,
shipment_id, shipment_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
site_ids=site_ids, site_ids=site_ids,
) )
@@ -792,6 +784,7 @@ class ShipmentService:
query_db, query_db,
locked_recovered, locked_recovered,
current_user, current_user,
test_scope=test_scope,
require_active=False, require_active=False,
) )
return cls._success('确认成功') return cls._success('确认成功')
@@ -805,13 +798,16 @@ class ShipmentService:
shipment_id: int, shipment_id: int,
page_object: ConfirmCommandModel, page_object: ConfirmCommandModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
user_id, user_name = cls._current_identity(current_user) user_id, user_name = cls._current_identity(current_user)
warehouse_ids, site_ids = await cls._scopes(query_db, current_user) warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
shipment = await OperationDao.get_shipment_by_id( shipment = await OperationDao.get_shipment_by_id(
query_db, query_db,
shipment_id, shipment_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
site_ids=site_ids, site_ids=site_ids,
) )
@@ -823,6 +819,7 @@ class ShipmentService:
existing = await OperationDao.get_shipment_by_confirm_idempotency_key( existing = await OperationDao.get_shipment_by_confirm_idempotency_key(
query_db, query_db,
idempotency_key, idempotency_key,
test_scope,
warehouse_ids=[shipment.source_warehouse_id], warehouse_ids=[shipment.source_warehouse_id],
site_ids=[shipment.target_site_id], site_ids=[shipment.target_site_id],
) )
@@ -837,6 +834,7 @@ class ShipmentService:
locked_shipment = await OperationDao.get_shipment_for_update( locked_shipment = await OperationDao.get_shipment_for_update(
query_db, query_db,
shipment_id, shipment_id,
test_scope,
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
site_ids=site_ids, site_ids=site_ids,
) )
@@ -853,6 +851,7 @@ class ShipmentService:
query_db, query_db,
locked_shipment, locked_shipment,
current_user, current_user,
test_scope=test_scope,
require_active=False, require_active=False,
) )
await query_db.rollback() await query_db.rollback()
@@ -874,8 +873,9 @@ class ShipmentService:
query_db, query_db,
ordered_ids, ordered_ids,
locked_shipment.source_warehouse_id, locked_shipment.source_warehouse_id,
test_scope,
) )
await cls._lock_and_reauthorize_route(query_db, locked_shipment, current_user) await cls._lock_and_reauthorize_route(query_db, locked_shipment, current_user, test_scope=test_scope)
for inventory in inventories: for inventory in inventories:
PalletStateMachine.ensure_transition( PalletStateMachine.ensure_transition(
@@ -930,6 +930,7 @@ class ShipmentService:
occurred_at=current_time, occurred_at=current_time,
idempotency_key=idempotency_key, idempotency_key=idempotency_key,
request_hash=request_hash, request_hash=request_hash,
test_batch_id=test_scope.batch_id,
) )
await InventoryDao.add_event(query_db, inventory, event) await InventoryDao.add_event(query_db, inventory, event)
@@ -943,6 +944,7 @@ class ShipmentService:
page_object.idempotency_key, page_object.idempotency_key,
cls._build_confirm_request_hash(shipment_id), cls._build_confirm_request_hash(shipment_id),
current_user, current_user,
test_scope,
) )
except Exception: except Exception:
await query_db.rollback() await query_db.rollback()
@@ -18,6 +18,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.operation_do import BizPalletEvent, BizUsageRecord from module_business.entity.do.operation_do import BizPalletEvent, BizUsageRecord
from module_business.entity.vo.master_vo import LinePageQueryModel from module_business.entity.vo.master_vo import LinePageQueryModel
from module_business.entity.vo.operation_vo import ( from module_business.entity.vo.operation_vo import (
@@ -29,6 +30,7 @@ from module_business.entity.vo.operation_vo import (
UsageSuccessResponseModel, UsageSuccessResponseModel,
) )
from module_business.service.access_service import AccessService from module_business.service.access_service import AccessService
from module_business.service.test_batch_access_service import TestBatchAccessService
class UsageService: class UsageService:
@@ -147,8 +149,9 @@ class UsageService:
query_db: AsyncSession, query_db: AsyncSession,
site_id: int, site_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> None: ) -> None:
site = await MasterDao.get_site(query_db, site_id, for_update=True) site = await MasterDao.get_site(query_db, site_id, test_scope, for_update=True)
try: try:
await AccessService.assert_site_access( await AccessService.assert_site_access(
query_db, query_db,
@@ -171,6 +174,7 @@ class UsageService:
request_hash: str, request_hash: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
expected_site_id: int, expected_site_id: int,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
try: try:
site_ids = await cls._site_scope(query_db, current_user) site_ids = await cls._site_scope(query_db, current_user)
@@ -179,6 +183,7 @@ class UsageService:
recovered = await OperationDao.get_usage_by_idempotency_key( recovered = await OperationDao.get_usage_by_idempotency_key(
query_db, query_db,
page_object.idempotency_key, page_object.idempotency_key,
test_scope,
site_ids=[expected_site_id], site_ids=[expected_site_id],
) )
if recovered is None: if recovered is None:
@@ -191,6 +196,7 @@ class UsageService:
query_db, query_db,
recovered.site_id, recovered.site_id,
current_user, current_user,
test_scope,
) )
return cls._success(recovered) return cls._success(recovered)
finally: finally:
@@ -202,6 +208,7 @@ class UsageService:
query_db: AsyncSession, query_db: AsyncSession,
query_object: UsagePageQueryModel, query_object: UsagePageQueryModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
is_page: bool = True, is_page: bool = True,
) -> PageModel | list[UsageModel]: ) -> PageModel | list[UsageModel]:
site_ids = await cls._site_scope(query_db, current_user) site_ids = await cls._site_scope(query_db, current_user)
@@ -220,8 +227,9 @@ class UsageService:
result = await OperationDao.get_usage_list( result = await OperationDao.get_usage_list(
query_db, query_db,
query_object, query_object,
is_page,
site_ids=site_ids, site_ids=site_ids,
scope=test_scope,
is_page=is_page,
) )
rows = result.rows if isinstance(result, PageModel) else result rows = result.rows if isinstance(result, PageModel) else result
models = [UsageModel.model_validate(record) for record in rows] models = [UsageModel.model_validate(record) for record in rows]
@@ -235,11 +243,13 @@ class UsageService:
query_db: AsyncSession, query_db: AsyncSession,
usage_id: int, usage_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> UsageModel: ) -> UsageModel:
site_ids = await cls._site_scope(query_db, current_user) site_ids = await cls._site_scope(query_db, current_user)
usage = await OperationDao.get_usage_by_id( usage = await OperationDao.get_usage_by_id(
query_db, query_db,
usage_id, usage_id,
test_scope,
site_ids=site_ids, site_ids=site_ids,
) )
if usage is None: if usage is None:
@@ -252,10 +262,11 @@ class UsageService:
query_db: AsyncSession, query_db: AsyncSession,
site_id: int, site_id: int,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> list[BusinessOptionModel]: ) -> list[BusinessOptionModel]:
cls._current_identity(current_user) cls._current_identity(current_user)
await AccessService.assert_site_access(query_db, current_user, site_id) await AccessService.assert_site_access(query_db, current_user, site_id)
site = await MasterDao.get_site(query_db, site_id) site = await MasterDao.get_site(query_db, site_id, test_scope)
if site is None: if site is None:
raise ServiceException(message='客户现场不存在') raise ServiceException(message='客户现场不存在')
if site.status != '0': if site.status != '0':
@@ -263,6 +274,7 @@ class UsageService:
lines = await MasterDao.list_line( lines = await MasterDao.list_line(
query_db, query_db,
LinePageQueryModel(siteId=site_id, status='0'), LinePageQueryModel(siteId=site_id, status='0'),
test_scope,
False, False,
) )
options = [ options = [
@@ -282,12 +294,13 @@ class UsageService:
query_db: AsyncSession, query_db: AsyncSession,
qr_token: str, qr_token: str,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> PalletScanResponseModel: ) -> PalletScanResponseModel:
normalized_qr = qr_token.strip() normalized_qr = qr_token.strip()
pallet = await InventoryDao.get_pallet_by_qr_token(query_db, normalized_qr) pallet = await InventoryDao.get_pallet_by_qr_token(query_db, normalized_qr, test_scope)
if pallet is None: if pallet is None:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id) inventory = await MasterDao.get_pallet_inventory(query_db, pallet.pallet_id, test_scope)
if inventory is None: if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.site_id is None: if inventory.site_id is None:
@@ -311,7 +324,7 @@ class UsageService:
or inventory.remaining_packages > pallet.capacity_packages or inventory.remaining_packages > pallet.capacity_packages
): ):
raise ServiceException(message='托盘当前状态不允许使用') raise ServiceException(message='托盘当前状态不允许使用')
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id) filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id, test_scope)
if filling is None: if filling is None:
raise ServiceException(message='托盘活动装填周期不存在') raise ServiceException(message='托盘活动装填周期不存在')
if ( if (
@@ -345,14 +358,16 @@ class UsageService:
query_db: AsyncSession, query_db: AsyncSession,
page_object: UsageCreateModel, page_object: UsageCreateModel,
current_user: CurrentUserModel, current_user: CurrentUserModel,
test_scope: TestBatchScope,
) -> CrudResponseModel: ) -> CrudResponseModel:
expected_site_id: int | None = None expected_site_id: int | None = None
try: try:
await TestBatchAccessService.lock_active_batch(query_db, test_scope)
request_hash = cls._build_request_hash(page_object) request_hash = cls._build_request_hash(page_object)
user_id, user_name = cls._current_identity(current_user) user_id, user_name = cls._current_identity(current_user)
preflight_site_ids = await cls._site_scope(query_db, current_user) preflight_site_ids = await cls._site_scope(query_db, current_user)
# 产线归属是请求内稳定路由;锁事务仍以库存中的实际现场复核。 # 产线归属是请求内稳定路由;锁事务仍以库存中的实际现场复核。
preflight_line = await MasterDao.get_line(query_db, page_object.line_id) preflight_line = await MasterDao.get_line(query_db, page_object.line_id, test_scope)
preflight_site_id = preflight_line.site_id if preflight_line is not None else None preflight_site_id = preflight_line.site_id if preflight_line is not None else None
existing = None existing = None
if preflight_site_id is not None and ( if preflight_site_id is not None and (
@@ -361,6 +376,7 @@ class UsageService:
existing = await OperationDao.get_usage_by_idempotency_key( existing = await OperationDao.get_usage_by_idempotency_key(
query_db, query_db,
page_object.idempotency_key, page_object.idempotency_key,
test_scope,
site_ids=[preflight_site_id], site_ids=[preflight_site_id],
) )
if existing is not None: if existing is not None:
@@ -372,6 +388,7 @@ class UsageService:
query_db, query_db,
existing.site_id, existing.site_id,
current_user, current_user,
test_scope,
) )
result = cls._success(existing) result = cls._success(existing)
await query_db.rollback() await query_db.rollback()
@@ -380,6 +397,7 @@ class UsageService:
scanned_pallet = await InventoryDao.get_pallet_by_qr_token( scanned_pallet = await InventoryDao.get_pallet_by_qr_token(
query_db, query_db,
page_object.qr_token.strip(), page_object.qr_token.strip(),
test_scope,
) )
if scanned_pallet is None: if scanned_pallet is None:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
@@ -389,6 +407,7 @@ class UsageService:
inventory = await InventoryDao.get_inventory_for_update( inventory = await InventoryDao.get_inventory_for_update(
query_db, query_db,
scanned_pallet_id, scanned_pallet_id,
test_scope,
) )
if inventory is None: if inventory is None:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
@@ -403,6 +422,7 @@ class UsageService:
locked_pallets = await InventoryDao.get_pallets_by_ids( locked_pallets = await InventoryDao.get_pallets_by_ids(
query_db, query_db,
[inventory.pallet_id], [inventory.pallet_id],
test_scope,
) )
if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != inventory.pallet_id: if len(locked_pallets) != 1 or locked_pallets[0].pallet_id != inventory.pallet_id:
cls._raise_pallet_not_accessible(current_user) cls._raise_pallet_not_accessible(current_user)
@@ -412,6 +432,7 @@ class UsageService:
await OperationDao.get_active_filling_for_update( await OperationDao.get_active_filling_for_update(
query_db, query_db,
inventory.active_filling_id, inventory.active_filling_id,
test_scope,
) )
if inventory.active_filling_id is not None if inventory.active_filling_id is not None
else None else None
@@ -419,6 +440,7 @@ class UsageService:
locked_existing = await OperationDao.get_usage_by_idempotency_key( locked_existing = await OperationDao.get_usage_by_idempotency_key(
query_db, query_db,
page_object.idempotency_key, page_object.idempotency_key,
test_scope,
site_ids=[inventory.site_id], site_ids=[inventory.site_id],
) )
if locked_existing is not None: if locked_existing is not None:
@@ -432,6 +454,7 @@ class UsageService:
query_db, query_db,
locked_existing.site_id, locked_existing.site_id,
current_user, current_user,
test_scope,
) )
result = cls._success(locked_existing) result = cls._success(locked_existing)
await query_db.rollback() await query_db.rollback()
@@ -442,7 +465,7 @@ class UsageService:
if filling is None: if filling is None:
raise ServiceException(message='托盘活动装填周期不存在或已关闭') raise ServiceException(message='托盘活动装填周期不存在或已关闭')
site = await MasterDao.get_site(query_db, inventory.site_id, for_update=True) site = await MasterDao.get_site(query_db, inventory.site_id, test_scope, for_update=True)
await cls._assert_pallet_site_access( await cls._assert_pallet_site_access(
query_db, query_db,
current_user, current_user,
@@ -453,7 +476,7 @@ class UsageService:
raise ServiceException(message='客户现场不存在') raise ServiceException(message='客户现场不存在')
if site.status != '0': if site.status != '0':
raise ServiceException(message='客户现场已停用') raise ServiceException(message='客户现场已停用')
line = await MasterDao.get_line(query_db, page_object.line_id, for_update=True) line = await MasterDao.get_line(query_db, page_object.line_id, test_scope, for_update=True)
if line is None: if line is None:
raise ServiceException(message='产线不存在') raise ServiceException(message='产线不存在')
if line.status != '0': if line.status != '0':
@@ -512,6 +535,7 @@ class UsageService:
request_hash=request_hash, request_hash=request_hash,
operator_id=user_id, operator_id=user_id,
occurred_at=current_time, occurred_at=current_time,
test_batch_id=test_scope.batch_id,
) )
usage = await OperationDao.add_usage_record(query_db, usage) usage = await OperationDao.add_usage_record(query_db, usage)
usage_id = usage.usage_id usage_id = usage.usage_id
@@ -553,6 +577,7 @@ class UsageService:
occurred_at=current_time, occurred_at=current_time,
idempotency_key=page_object.idempotency_key, idempotency_key=page_object.idempotency_key,
request_hash=request_hash, request_hash=request_hash,
test_batch_id=test_scope.batch_id,
) )
await InventoryDao.add_event(query_db, inventory, event) await InventoryDao.add_event(query_db, inventory, event)
await query_db.commit() await query_db.commit()
@@ -567,6 +592,7 @@ class UsageService:
request_hash, request_hash,
current_user, current_user,
expected_site_id, expected_site_id,
test_scope,
) )
except Exception: except Exception:
await query_db.rollback() await query_db.rollback()
@@ -25,6 +25,7 @@ from module_business.dao.inventory_dao import InventoryDao
from module_business.dao.master_dao import MasterDao from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet, BizStrain from module_business.entity.do.master_do import BizPallet, BizStrain
from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory from module_business.entity.do.operation_do import BizFillingRecord, BizPalletEvent, BizPalletInventory
from module_business.entity.vo.operation_vo import ( from module_business.entity.vo.operation_vo import (
@@ -160,13 +161,24 @@ def make_filling(
class TestFillingPublicContract(unittest.TestCase): class TestFillingPublicContract(unittest.TestCase):
def test_public_business_methods_require_explicit_test_scope(self) -> None:
for method_name in (
'get_filling_list_services',
'filling_detail_services',
'get_strain_options_services',
'scan_filling_pallet_services',
'fill_pallet_services',
):
with self.subTest(method=method_name):
self.assertIn('test_scope', inspect.signature(getattr(FillingService, method_name)).parameters)
def test_service_and_controller_are_available(self) -> None: def test_service_and_controller_are_available(self) -> None:
self.assertIsNotNone(FillingService) self.assertIsNotNone(FillingService)
self.assertIsNotNone(filling_controller_module.filling_controller) self.assertIsNotNone(filling_controller_module.filling_controller)
def test_fill_signature_and_types_are_exact(self) -> None: def test_fill_signature_and_types_are_exact(self) -> None:
signature = inspect.signature(FillingService.fill_pallet_services) signature = inspect.signature(FillingService.fill_pallet_services)
self.assertEqual(('query_db', 'page_object', 'current_user'), tuple(signature.parameters)) self.assertEqual(('query_db', 'page_object', 'current_user', 'test_scope'), tuple(signature.parameters))
hints = get_type_hints(FillingService.fill_pallet_services) hints = get_type_hints(FillingService.fill_pallet_services)
self.assertIs(AsyncSession, hints['query_db']) self.assertIs(AsyncSession, hints['query_db'])
self.assertIs(FillingCreateModel, hints['page_object']) self.assertIs(FillingCreateModel, hints['page_object'])
@@ -217,9 +229,15 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[9, 7, 7])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[9, 7, 7])),
patch.object(OperationDao, 'get_filling_list', new=get_list), patch.object(OperationDao, 'get_filling_list', new=get_list),
): ):
result = await FillingService.get_filling_list_services(self.db, query, self.user) result = await FillingService.get_filling_list_services(self.db, query, self.user, TestBatchScope.formal())
get_list.assert_awaited_once_with(self.db, query, True, warehouse_ids=[7, 9]) get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=[7, 9],
scope=TestBatchScope.formal(),
is_page=True,
)
self.assertIsInstance(result.rows[0], FillingModel) self.assertIsInstance(result.rows[0], FillingModel)
self.assertEqual(1, result.total) self.assertEqual(1, result.total)
@@ -231,9 +249,15 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[])),
patch.object(OperationDao, 'get_filling_list', new=get_list), patch.object(OperationDao, 'get_filling_list', new=get_list),
): ):
result = await FillingService.get_filling_list_services(self.db, query, self.user) result = await FillingService.get_filling_list_services(self.db, query, self.user, TestBatchScope.formal())
get_list.assert_awaited_once_with(self.db, query, True, warehouse_ids=[]) get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=[],
scope=TestBatchScope.formal(),
is_page=True,
)
self.assertEqual([], result.rows) self.assertEqual([], result.rows)
self.assertEqual(0, result.total) self.assertEqual(0, result.total)
@@ -245,7 +269,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_list', new=get_list), patch.object(OperationDao, 'get_filling_list', new=get_list),
self.assertRaises(PermissionException), self.assertRaises(PermissionException),
): ):
await FillingService.get_filling_list_services(self.db, query, self.user) await FillingService.get_filling_list_services(self.db, query, self.user, TestBatchScope.formal())
get_list.assert_not_awaited() get_list.assert_not_awaited()
async def test_admin_list_is_unscoped(self) -> None: async def test_admin_list_is_unscoped(self) -> None:
@@ -256,9 +280,17 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope), patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
patch.object(OperationDao, 'get_filling_list', new=get_list), patch.object(OperationDao, 'get_filling_list', new=get_list),
): ):
result = await FillingService.get_filling_list_services(self.db, query, make_user(1), is_page=False) result = await FillingService.get_filling_list_services(
self.db, query, make_user(1), TestBatchScope.formal(), is_page=False
)
scope.assert_not_awaited() scope.assert_not_awaited()
get_list.assert_awaited_once_with(self.db, query, False, warehouse_ids=None) get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=None,
scope=TestBatchScope.formal(),
is_page=False,
)
self.assertEqual([], result) self.assertEqual([], result)
async def test_detail_is_scoped_before_lookup_and_hides_missing_for_normal_user(self) -> None: async def test_detail_is_scoped_before_lookup_and_hides_missing_for_normal_user(self) -> None:
@@ -268,8 +300,8 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_id', new=get_detail), patch.object(OperationDao, 'get_filling_by_id', new=get_detail),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await FillingService.filling_detail_services(self.db, 31, self.user) await FillingService.filling_detail_services(self.db, 31, self.user, TestBatchScope.formal())
get_detail.assert_awaited_once_with(self.db, 31, warehouse_ids=[7]) get_detail.assert_awaited_once_with(self.db, 31, TestBatchScope.formal(), warehouse_ids=[7])
self.assertEqual('无权访问该装填记录', raised.exception.message) self.assertEqual('无权访问该装填记录', raised.exception.message)
async def test_admin_detail_keeps_precise_not_found_error(self) -> None: async def test_admin_detail_keeps_precise_not_found_error(self) -> None:
@@ -278,8 +310,8 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_id', new=get_detail), patch.object(OperationDao, 'get_filling_by_id', new=get_detail),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await FillingService.filling_detail_services(self.db, 31, make_user(1)) await FillingService.filling_detail_services(self.db, 31, make_user(1), TestBatchScope.formal())
get_detail.assert_awaited_once_with(self.db, 31, warehouse_ids=None) get_detail.assert_awaited_once_with(self.db, 31, TestBatchScope.formal(), warehouse_ids=None)
self.assertEqual('装填记录不存在', raised.exception.message) self.assertEqual('装填记录不存在', raised.exception.message)
async def test_strain_options_return_only_enabled_business_options(self) -> None: async def test_strain_options_return_only_enabled_business_options(self) -> None:
@@ -290,11 +322,12 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
] ]
list_strain = AsyncMock(return_value=strains) list_strain = AsyncMock(return_value=strains)
with patch.object(MasterDao, 'list_strain', new=list_strain): with patch.object(MasterDao, 'list_strain', new=list_strain):
result = await FillingService.get_strain_options_services(self.db, self.user) result = await FillingService.get_strain_options_services(self.db, self.user, TestBatchScope.formal())
query = list_strain.await_args.args[1] query = list_strain.await_args.args[1]
self.assertEqual('0', query.status) self.assertEqual('0', query.status)
self.assertFalse(list_strain.await_args.args[2]) self.assertEqual(TestBatchScope.formal(), list_strain.await_args.args[2])
self.assertFalse(list_strain.await_args.args[3])
self.assertEqual([1, 3], [option.id for option in result]) self.assertEqual([1, 3], [option.id for option in result])
self.assertTrue(all(isinstance(option, BusinessOptionModel) for option in result)) self.assertTrue(all(isinstance(option, BusinessOptionModel) for option in result))
@@ -311,7 +344,9 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock), patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock),
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock), patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock),
): ):
result = await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user) result = await FillingService.scan_filling_pallet_services(
self.db, 'qr-2', self.user, TestBatchScope.formal()
)
access.assert_awaited_once_with(self.db, self.user, 7) access.assert_awaited_once_with(self.db, self.user, 7)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
@@ -334,7 +369,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_warehouse_access', new=access), patch.object(AccessService, 'assert_warehouse_access', new=access),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user) await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user, TestBatchScope.formal())
access.assert_awaited_once_with(self.db, self.user, 7) access.assert_awaited_once_with(self.db, self.user, 7)
self.assertEqual('', raised.exception.data) self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
@@ -346,7 +381,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
self.assertRaises(exception_type), self.assertRaises(exception_type),
): ):
await FillingService.scan_filling_pallet_services(self.db, 'missing', user) await FillingService.scan_filling_pallet_services(self.db, 'missing', user, TestBatchScope.formal())
async def test_scan_admin_distinguishes_missing_inventory(self) -> None: async def test_scan_admin_distinguishes_missing_inventory(self) -> None:
with ( with (
@@ -354,7 +389,7 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=None)), patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=None)),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', make_user(1)) await FillingService.scan_filling_pallet_services(self.db, 'qr-2', make_user(1), TestBatchScope.formal())
self.assertEqual('托盘库存不存在', raised.exception.message) self.assertEqual('托盘库存不存在', raised.exception.message)
@@ -441,7 +476,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch('module_business.service.filling_service.datetime') as mocked_datetime, patch('module_business.service.filling_service.datetime') as mocked_datetime,
): ):
mocked_datetime.now.return_value = self.fixed_time mocked_datetime.now.return_value = self.fixed_time
result = await FillingService.fill_pallet_services(self.db, self.page, self.user) result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual( self.assertEqual(
[ [
@@ -460,8 +495,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
[False, False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list] [False, False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list]
) )
self.assertEqual([7, 7, 7], [call.args[2] for call in access.await_args_list]) self.assertEqual([7, 7, 7], [call.args[2] for call in access.await_args_list])
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_read.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
strain_read.assert_awaited_once_with(self.db, 11, for_update=True) strain_read.assert_awaited_once_with(self.db, 11, TestBatchScope.formal(), for_update=True)
filling = captured['filling'] filling = captured['filling']
self.assertTrue(filling.filling_no.startswith('FL-')) self.assertTrue(filling.filling_no.startswith('FL-'))
self.assertEqual(2, filling.pallet_id) self.assertEqual(2, filling.pallet_id)
@@ -539,7 +574,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await FillingService.fill_pallet_services(self.db, page or self.page, self.user) await FillingService.fill_pallet_services(self.db, page or self.page, self.user, TestBatchScope.formal())
add_filling.assert_not_awaited() add_filling.assert_not_awaited()
add_event.assert_not_awaited() add_event.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -580,11 +615,11 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('仓库已停用', raised.exception.message) self.assertEqual('仓库已停用', raised.exception.message)
self.assertEqual(['inventory-lock', 'pallet-lock', 'warehouse-read'], actions) self.assertEqual(['inventory-lock', 'pallet-lock', 'warehouse-read'], actions)
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_read.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
strain_read.assert_not_awaited() strain_read.assert_not_awaited()
add_filling.assert_not_awaited() add_filling.assert_not_awaited()
add_event.assert_not_awaited() add_event.assert_not_awaited()
@@ -639,7 +674,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('', raised.exception.data) self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
@@ -651,7 +686,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
[False, False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list] [False, False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list]
) )
self.assertEqual([7, 7, 7], [call.args[2] for call in access.await_args_list]) self.assertEqual([7, 7, 7], [call.args[2] for call in access.await_args_list])
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_read.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
strain_read.assert_not_awaited() strain_read.assert_not_awaited()
add_filling.assert_not_awaited() add_filling.assert_not_awaited()
add_event.assert_not_awaited() add_event.assert_not_awaited()
@@ -676,7 +711,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=add_filling), patch.object(OperationDao, 'add_filling_record', new=add_filling),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
add_filling.assert_not_awaited() add_filling.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -717,9 +752,14 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_warehouse', new=warehouse_read), patch.object(MasterDao, 'get_warehouse', new=warehouse_read),
patch.object(MasterDao, 'get_strain', new=strain_read), patch.object(MasterDao, 'get_strain', new=strain_read),
): ):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user) result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
get_key.assert_awaited_once_with(self.db, 'fill-key', warehouse_ids=[7]) get_key.assert_awaited_once_with(
self.db,
'fill-key',
TestBatchScope.formal(),
warehouse_ids=[7],
)
access.assert_awaited_once_with(self.db, self.user, 7) access.assert_awaited_once_with(self.db, self.user, 7)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
warehouse_read.assert_not_awaited() warehouse_read.assert_not_awaited()
@@ -741,9 +781,16 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()), patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=get_key), patch.object(OperationDao, 'get_filling_by_idempotency_key', new=get_key),
): ):
result = await FillingService.fill_pallet_services(self.db, self.page, user) result = await FillingService.fill_pallet_services(
self.db, self.page, user, TestBatchScope.formal()
)
get_key.assert_awaited_once_with(self.db, 'fill-key', warehouse_ids=[7]) get_key.assert_awaited_once_with(
self.db,
'fill-key',
TestBatchScope.formal(),
warehouse_ids=[7],
)
self.assertEqual(31, result.result) self.assertEqual(31, result.result)
if user.user and user.user.admin: if user.user and user.user.admin:
scope_read.assert_not_awaited() scope_read.assert_not_awaited()
@@ -756,7 +803,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=existing)), patch.object(OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=existing)),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertIn('幂等键', raised.exception.message) self.assertIn('幂等键', raised.exception.message)
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -767,7 +814,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=get_key), patch.object(OperationDao, 'get_filling_by_idempotency_key', new=get_key),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('', raised.exception.data) self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该仓库', raised.exception.message) self.assertEqual('无权访问该仓库', raised.exception.message)
get_key.assert_not_awaited() get_key.assert_not_awaited()
@@ -802,7 +849,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual([7, foreign_warehouse_id], [call.args[2] for call in access.await_args_list]) self.assertEqual([7, foreign_warehouse_id], [call.args[2] for call in access.await_args_list])
self.assertEqual('', raised.exception.data) self.assertEqual('', raised.exception.data)
@@ -856,7 +903,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=add_filling), patch.object(OperationDao, 'add_filling_record', new=add_filling),
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
): ):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user) result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(['inventory-lock', 'pallet-lock', 'filling-lock'], actions) self.assertEqual(['inventory-lock', 'pallet-lock', 'filling-lock'], actions)
self.assertEqual(2, get_key.await_count) self.assertEqual(2, get_key.await_count)
@@ -893,7 +940,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=add_filling), patch.object(OperationDao, 'add_filling_record', new=add_filling),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('装填失败,幂等键已被其他请求使用', raised.exception.message) self.assertEqual('装填失败,幂等键已被其他请求使用', raised.exception.message)
self.assertEqual(2, get_key.await_count) self.assertEqual(2, get_key.await_count)
@@ -914,7 +961,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=RuntimeError('flush failed'))), patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=RuntimeError('flush failed'))),
self.assertRaises(RuntimeError), self.assertRaises(RuntimeError),
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -923,7 +970,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(FillingService, '_build_request_hash', side_effect=RuntimeError('hash failed')), patch.object(FillingService, '_build_request_hash', side_effect=RuntimeError('hash failed')),
self.assertRaises(RuntimeError), self.assertRaises(RuntimeError),
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -942,7 +989,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=self.strain)), patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=self.strain)),
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())), patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
): ):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user) result = await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(2, key_reads.await_count) self.assertEqual(2, key_reads.await_count)
self.assertEqual( self.assertEqual(
@@ -968,7 +1015,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())), patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该仓库', raised.exception.message) self.assertEqual('无权访问该仓库', raised.exception.message)
self.assertEqual(2, warehouse_scope.await_count) self.assertEqual(2, warehouse_scope.await_count)
@@ -993,7 +1040,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())), patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('装填失败,数据冲突', raised.exception.message) self.assertEqual('装填失败,数据冲突', raised.exception.message)
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -1009,7 +1056,13 @@ class TestFillingOperationDaoScope(unittest.IsolatedAsyncioTestCase):
paginate = AsyncMock(return_value=[]) paginate = AsyncMock(return_value=[])
db = MagicMock(spec=AsyncSession) db = MagicMock(spec=AsyncSession)
with patch.object(OperationDao, '_paginate', new=paginate): with patch.object(OperationDao, '_paginate', new=paginate):
await OperationDao.get_filling_list(db, query, False, warehouse_ids=[9, 7, 7]) await OperationDao.get_filling_list(
db,
query,
warehouse_ids=[9, 7, 7],
scope=TestBatchScope.formal(),
is_page=False,
)
sql = self._compiled(paginate.await_args.args[1]) sql = self._compiled(paginate.await_args.args[1])
self.assertIn('biz_filling_record.warehouse_id in (7, 9)', sql) self.assertIn('biz_filling_record.warehouse_id in (7, 9)', sql)
@@ -1018,7 +1071,13 @@ class TestFillingOperationDaoScope(unittest.IsolatedAsyncioTestCase):
paginate = AsyncMock(return_value=[]) paginate = AsyncMock(return_value=[])
db = MagicMock(spec=AsyncSession) db = MagicMock(spec=AsyncSession)
with patch.object(OperationDao, '_paginate', new=paginate): with patch.object(OperationDao, '_paginate', new=paginate):
await OperationDao.get_filling_list(db, query, False, warehouse_ids=[]) await OperationDao.get_filling_list(
db,
query,
warehouse_ids=[],
scope=TestBatchScope.formal(),
is_page=False,
)
sql = self._compiled(paginate.await_args.args[1]) sql = self._compiled(paginate.await_args.args[1])
self.assertTrue('false' in sql or '0 = 1' in sql, sql) self.assertTrue('false' in sql or '0 = 1' in sql, sql)
@@ -1032,7 +1091,7 @@ class TestFillingOperationDaoScope(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = None result.scalar_one_or_none.return_value = None
db = MagicMock(spec=AsyncSession) db = MagicMock(spec=AsyncSession)
db.execute.return_value = result db.execute.return_value = result
await method(db, key, warehouse_ids=[7]) await method(db, key, TestBatchScope.formal(), warehouse_ids=[7])
sql = self._compiled(db.execute.await_args.args[0]) sql = self._compiled(db.execute.await_args.args[0])
self.assertIn('biz_filling_record.warehouse_id in (7)', sql) self.assertIn('biz_filling_record.warehouse_id in (7)', sql)
@@ -22,7 +22,7 @@ from common.vo import (
PageResponseModel, PageResponseModel,
ResponseBaseModel, ResponseBaseModel,
) )
from exceptions.exception import PermissionException, ServiceException from exceptions.exception import PermissionException, ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel, UserInfoModel from module_admin.entity.vo.user_vo import CurrentUserModel, UserInfoModel
from module_admin.service.log_service import LogQueueService from module_admin.service.log_service import LogQueueService
from module_admin.service.login_service import LoginService from module_admin.service.login_service import LoginService
@@ -33,6 +33,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizInboundItem, BizInboundItem,
@@ -129,6 +130,20 @@ def make_inbound(
class TestInboundPublicContract(unittest.TestCase): class TestInboundPublicContract(unittest.TestCase):
def test_public_business_methods_require_explicit_test_scope(self) -> None:
for method_name in (
'get_inbound_list_services',
'inbound_detail_services',
'get_warehouse_options_services',
'scan_inbound_pallet_services',
'add_inbound_services',
'edit_inbound_services',
'delete_inbound_services',
'confirm_inbound_services',
):
with self.subTest(method=method_name):
self.assertIn('test_scope', inspect.signature(getattr(InboundService, method_name)).parameters)
def test_service_and_controller_are_available(self) -> None: def test_service_and_controller_are_available(self) -> None:
self.assertIsNotNone(InboundService) self.assertIsNotNone(InboundService)
self.assertIsNotNone(inbound_controller_module.inbound_controller) self.assertIsNotNone(inbound_controller_module.inbound_controller)
@@ -136,7 +151,7 @@ class TestInboundPublicContract(unittest.TestCase):
def test_confirm_signature_and_types_are_exact(self) -> None: def test_confirm_signature_and_types_are_exact(self) -> None:
signature = inspect.signature(InboundService.confirm_inbound_services) signature = inspect.signature(InboundService.confirm_inbound_services)
self.assertEqual( self.assertEqual(
('query_db', 'inbound_id', 'page_object', 'current_user'), ('query_db', 'inbound_id', 'page_object', 'current_user', 'test_scope'),
tuple(signature.parameters), tuple(signature.parameters),
) )
hints = get_type_hints(InboundService.confirm_inbound_services) hints = get_type_hints(InboundService.confirm_inbound_services)
@@ -228,7 +243,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(inbound_service_module, 'datetime') as service_datetime, patch.object(inbound_service_module, 'datetime') as service_datetime,
): ):
service_datetime.now.return_value = self.fixed_time service_datetime.now.return_value = self.fixed_time
result = await InboundService.add_inbound_services(self.db, page, self.user) result = await InboundService.add_inbound_services(self.db, page, self.user, TestBatchScope.formal())
self.assertEqual( self.assertEqual(
[ [
@@ -242,8 +257,8 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
], ],
actions, actions,
) )
inventory_lock.assert_awaited_once_with(self.db, [2, 5]) inventory_lock.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
pallet_lock.assert_awaited_once_with(self.db, [2, 5]) pallet_lock.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
token_hex.assert_called_once_with(16) token_hex.assert_called_once_with(16)
order = captured['order'] order = captured['order']
self.assertEqual('IN-fixed-random', order.inbound_no) self.assertEqual('IN-fixed-random', order.inbound_no)
@@ -288,7 +303,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.add_inbound_services(self.db, page, self.user) await InboundService.add_inbound_services(self.db, page, self.user, TestBatchScope.formal())
add_order.assert_not_awaited() add_order.assert_not_awaited()
replace_items.assert_not_awaited() replace_items.assert_not_awaited()
@@ -313,7 +328,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=lock), patch.object(InventoryDao, 'get_inventories_for_update', new=lock),
self.assertRaises(exception_type), self.assertRaises(exception_type),
): ):
await InboundService.add_inbound_services(self.db, page, self.user) await InboundService.add_inbound_services(self.db, page, self.user, TestBatchScope.formal())
lock.assert_not_awaited() lock.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -333,7 +348,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=lock), patch.object(InventoryDao, 'get_inventories_for_update', new=lock),
self.assertRaises(PermissionException), self.assertRaises(PermissionException),
): ):
await InboundService.add_inbound_services(self.db, page, self.user) await InboundService.add_inbound_services(self.db, page, self.user, TestBatchScope.formal())
lock.assert_not_awaited() lock.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -351,7 +366,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=pallet_lock), patch.object(InventoryDao, 'get_inventories_for_update', new=pallet_lock),
self.assertRaises(Exception) as raised, self.assertRaises(Exception) as raised,
): ):
await InboundService.add_inbound_services(self.db, page, self.user) await InboundService.add_inbound_services(self.db, page, self.user, TestBatchScope.formal())
self.assertIsInstance(raised.exception, PermissionException) self.assertIsInstance(raised.exception, PermissionException)
self.assertEqual('无权访问该仓库', raised.exception.message) self.assertEqual('无权访问该仓库', raised.exception.message)
@@ -385,11 +400,11 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'replace_inbound_items', new=replace_items), patch.object(OperationDao, 'replace_inbound_items', new=replace_items),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await InboundService.add_inbound_services(self.db, page, self.user) await InboundService.add_inbound_services(self.db, page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该仓库', raised.exception.message) self.assertEqual('无权访问该仓库', raised.exception.message)
self.assertEqual(2, warehouse_read.await_count) self.assertEqual(2, warehouse_read.await_count)
warehouse_read.assert_awaited_with(self.db, 7, for_update=True) warehouse_read.assert_awaited_with(self.db, 7, TestBatchScope.formal(), for_update=True)
self.assertEqual(2, warehouse_mapping.await_count) self.assertEqual(2, warehouse_mapping.await_count)
warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True) warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True)
add_order.assert_not_awaited() add_order.assert_not_awaited()
@@ -427,12 +442,13 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(inbound_service_module, 'datetime') as service_datetime, patch.object(inbound_service_module, 'datetime') as service_datetime,
): ):
service_datetime.now.return_value = self.fixed_time service_datetime.now.return_value = self.fixed_time
result = await InboundService.edit_inbound_services(self.db, page, self.user) result = await InboundService.edit_inbound_services(self.db, page, self.user, TestBatchScope.formal())
self.default_scope.assert_awaited_once_with(self.db, 7) self.default_scope.assert_awaited_once_with(self.db, 7)
lock_order.assert_awaited_once_with( lock_order.assert_awaited_once_with(
self.db, self.db,
11, 11,
TestBatchScope.formal(),
warehouse_ids=[2, 5, 7, 8, 11], warehouse_ids=[2, 5, 7, 8, 11],
) )
self.assertEqual( self.assertEqual(
@@ -443,8 +459,8 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
], ],
access.await_args_list, access.await_args_list,
) )
inventory_lock.assert_awaited_once_with(self.db, [2, 5]) inventory_lock.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
pallet_lock.assert_awaited_once_with(self.db, [2, 5]) pallet_lock.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
self.assertEqual(8, inbound.warehouse_id) self.assertEqual(8, inbound.warehouse_id)
self.assertEqual('user-7', inbound.update_by) self.assertEqual('user-7', inbound.update_by)
self.assertEqual(self.fixed_time, inbound.update_time) self.assertEqual(self.fixed_time, inbound.update_time)
@@ -478,7 +494,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
MasterDao, MasterDao,
'get_warehouse', 'get_warehouse',
new=AsyncMock( new=AsyncMock(
side_effect=lambda _db, warehouse_id, **_kwargs: SimpleNamespace( side_effect=lambda _db, warehouse_id, _scope, **_kwargs: SimpleNamespace(
warehouse_id=warehouse_id, warehouse_id=warehouse_id,
status='0', status='0',
) )
@@ -493,7 +509,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'replace_inbound_items', new=replace), patch.object(OperationDao, 'replace_inbound_items', new=replace),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await InboundService.edit_inbound_services(self.db, page, self.user) await InboundService.edit_inbound_services(self.db, page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该入库单', raised.exception.message) self.assertEqual('无权访问该入库单', raised.exception.message)
warehouse_mapping.assert_any_await(self.db, self.user, 7, for_update=True) warehouse_mapping.assert_any_await(self.db, self.user, 7, for_update=True)
@@ -519,7 +535,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'update_inbound', new=update), patch.object(OperationDao, 'update_inbound', new=update),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.edit_inbound_services(self.db, page, self.user) await InboundService.edit_inbound_services(self.db, page, self.user, TestBatchScope.formal())
lock_pallets.assert_not_awaited() lock_pallets.assert_not_awaited()
update.assert_not_awaited() update.assert_not_awaited()
@@ -537,12 +553,12 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock), patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
self.assertRaises(Exception) as raised, self.assertRaises(Exception) as raised,
): ):
await InboundService.edit_inbound_services(self.db, page, self.user) await InboundService.edit_inbound_services(self.db, page, self.user, TestBatchScope.formal())
self.assertIsInstance(raised.exception, PermissionException) self.assertIsInstance(raised.exception, PermissionException)
self.assertEqual('无权访问该入库单', raised.exception.message) self.assertEqual('无权访问该入库单', raised.exception.message)
scope.assert_awaited_once_with(self.db, 7) scope.assert_awaited_once_with(self.db, 7)
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=[7]) lock_order.assert_awaited_once_with(self.db, 404, TestBatchScope.formal(), warehouse_ids=[7])
get_warehouse.assert_not_awaited() get_warehouse.assert_not_awaited()
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -556,130 +572,54 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order), patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await InboundService.edit_inbound_services(self.db, page, make_user(1)) await InboundService.edit_inbound_services(self.db, page, make_user(1), TestBatchScope.formal())
self.assertEqual('入库单不存在', raised.exception.message) self.assertEqual('入库单不存在', raised.exception.message)
scope.assert_not_awaited() scope.assert_not_awaited()
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=None) lock_order.assert_awaited_once_with(self.db, 404, TestBatchScope.formal(), warehouse_ids=None)
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
async def test_batch_delete_locks_and_preflights_all_before_ordered_delete(self) -> None: async def test_batch_delete_locks_and_preflights_all_before_ordered_delete(self) -> None:
actions: list[str] = [] lock = AsyncMock()
delete = AsyncMock()
async def lock(
_db: AsyncSession,
inbound_id: int,
warehouse_ids: list[int] | None = None,
) -> BizInboundOrder:
self.assertEqual([2, 5, 7, 8, 11], warehouse_ids)
actions.append(f'lock-{inbound_id}')
return make_inbound(inbound_id=inbound_id, warehouse_id=inbound_id)
async def delete(_db: AsyncSession, inbound_id: int) -> None:
actions.append(f'delete-{inbound_id}')
async def commit() -> None:
actions.append('commit')
self.db.commit.side_effect = commit
access = AsyncMock()
with ( with (
patch.object(OperationDao, 'get_inbound_for_update', new=AsyncMock(side_effect=lock)), patch.object(OperationDao, 'get_inbound_for_update', new=lock),
patch.object( patch.object(OperationDao, 'delete_inbound', new=delete),
MasterDao, self.assertRaises(ServiceWarning),
'get_warehouse',
new=AsyncMock(
side_effect=lambda _db, warehouse_id, **_kwargs: SimpleNamespace(
warehouse_id=warehouse_id,
status='0',
)
),
),
patch.object(AccessService, 'assert_warehouse_access', new=access),
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=delete)),
): ):
result = await InboundService.delete_inbound_services(self.db, [5, 2], self.user) await InboundService.delete_inbound_services(self.db, [5, 2], self.user, TestBatchScope.formal())
self.assertEqual( lock.assert_not_awaited()
['lock-2', 'lock-5', 'delete-2', 'delete-5', 'commit'], delete.assert_not_awaited()
actions, self.db.commit.assert_not_awaited()
)
self.default_scope.assert_awaited_once_with(self.db, 7)
self.assertEqual([2, 5], [call.args[2] for call in access.await_args_list])
self.assertTrue(all(call.kwargs.get('for_update') for call in access.await_args_list))
self.assertTrue(result.is_success)
self.db.rollback.assert_not_awaited()
async def test_batch_delete_rechecks_all_current_warehouses_before_first_delete(self) -> None: async def test_batch_delete_rechecks_all_current_warehouses_before_first_delete(self) -> None:
revoked_warehouse_id = 8 lock = AsyncMock()
documents = {
11: make_inbound(inbound_id=11, warehouse_id=revoked_warehouse_id),
12: make_inbound(inbound_id=12, warehouse_id=7),
}
async def warehouse_access(
_db: AsyncSession,
_user: CurrentUserModel,
warehouse_id: int,
**kwargs: Any,
) -> None:
if warehouse_id == revoked_warehouse_id and kwargs.get('for_update'):
raise PermissionException(data='', message='无权访问该仓库')
delete = AsyncMock() delete = AsyncMock()
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
with ( with (
patch.object( patch.object(OperationDao, 'get_inbound_for_update', new=lock),
OperationDao,
'get_inbound_for_update',
new=AsyncMock(side_effect=lambda _db, inbound_id, **_kwargs: documents[inbound_id]),
),
patch.object(
MasterDao,
'get_warehouse',
new=AsyncMock(
side_effect=lambda _db, warehouse_id, **_kwargs: SimpleNamespace(
warehouse_id=warehouse_id,
status='0',
)
),
),
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
patch.object(OperationDao, 'delete_inbound', new=delete), patch.object(OperationDao, 'delete_inbound', new=delete),
self.assertRaises(PermissionException) as raised, self.assertRaises(ServiceWarning),
): ):
await InboundService.delete_inbound_services(self.db, [12, 11], self.user) await InboundService.delete_inbound_services(self.db, [12, 11], self.user, TestBatchScope.formal())
self.assertEqual('无权访问该入库单', raised.exception.message) lock.assert_not_awaited()
self.assertEqual([7, 8], [call.args[2] for call in warehouse_mapping.await_args_list])
self.assertTrue(all(call.kwargs.get('for_update') for call in warehouse_mapping.await_args_list))
delete.assert_not_awaited() delete.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_batch_delete_is_atomic_when_later_document_is_confirmed(self) -> None: async def test_batch_delete_is_atomic_when_later_document_is_confirmed(self) -> None:
lock = AsyncMock()
delete = AsyncMock() delete = AsyncMock()
records = {
2: make_inbound(inbound_id=2, warehouse_id=2),
5: make_inbound(inbound_id=5, warehouse_id=5, status='CONFIRMED'),
}
with ( with (
patch.object( patch.object(OperationDao, 'get_inbound_for_update', new=lock),
OperationDao,
'get_inbound_for_update',
new=AsyncMock(
side_effect=lambda _db, inbound_id, warehouse_ids=None: records[inbound_id],
),
),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(OperationDao, 'delete_inbound', new=delete), patch.object(OperationDao, 'delete_inbound', new=delete),
self.assertRaises(ServiceException), self.assertRaises(ServiceWarning),
): ):
await InboundService.delete_inbound_services(self.db, [5, 2], self.user) await InboundService.delete_inbound_services(self.db, [5, 2], self.user, TestBatchScope.formal())
lock.assert_not_awaited()
delete.assert_not_awaited() delete.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_delete_scopes_parent_lock_and_hides_unmatched_document(self) -> None: async def test_delete_scopes_parent_lock_and_hides_unmatched_document(self) -> None:
lock_order = AsyncMock(return_value=None) lock_order = AsyncMock(return_value=None)
@@ -688,16 +628,13 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])) as scope, patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])) as scope,
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order), patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
patch.object(OperationDao, 'delete_inbound', new=delete), patch.object(OperationDao, 'delete_inbound', new=delete),
self.assertRaises(Exception) as raised, self.assertRaises(ServiceWarning),
): ):
await InboundService.delete_inbound_services(self.db, [404], self.user) await InboundService.delete_inbound_services(self.db, [404], self.user, TestBatchScope.formal())
self.assertIsInstance(raised.exception, PermissionException) scope.assert_not_awaited()
self.assertEqual('无权访问该入库单', raised.exception.message) lock_order.assert_not_awaited()
scope.assert_awaited_once_with(self.db, 7)
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=[7])
delete.assert_not_awaited() delete.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_admin_missing_delete_keeps_precise_not_found(self) -> None: async def test_admin_missing_delete_keeps_precise_not_found(self) -> None:
scope = AsyncMock() scope = AsyncMock()
@@ -705,14 +642,12 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope), patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order), patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceWarning),
): ):
await InboundService.delete_inbound_services(self.db, [404], make_user(1)) await InboundService.delete_inbound_services(self.db, [404], make_user(1), TestBatchScope.formal())
self.assertEqual('入库单不存在', raised.exception.message)
scope.assert_not_awaited() scope.assert_not_awaited()
lock_order.assert_awaited_once_with(self.db, 404, warehouse_ids=None) lock_order.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_all_draft_mutations_convert_integrity_errors(self) -> None: async def test_all_draft_mutations_convert_integrity_errors(self) -> None:
add_page = InboundCreateModel(warehouseId=7, palletIds=[2]) add_page = InboundCreateModel(warehouseId=7, palletIds=[2])
@@ -732,7 +667,7 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_inbound', new=AsyncMock(side_effect=integrity_error())), patch.object(OperationDao, 'add_inbound', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.add_inbound_services(self.db, add_page, self.user) await InboundService.add_inbound_services(self.db, add_page, self.user, TestBatchScope.formal())
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
self.db.reset_mock() self.db.reset_mock()
@@ -749,10 +684,9 @@ class TestInboundDraftService(unittest.IsolatedAsyncioTestCase):
), ),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()), patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=integrity_error())), patch.object(OperationDao, 'delete_inbound', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(ServiceException), self.assertRaises(ServiceWarning),
): ):
await InboundService.delete_inbound_services(self.db, [11], self.user) await InboundService.delete_inbound_services(self.db, [11], self.user, TestBatchScope.formal())
self.db.rollback.assert_awaited_once()
class TestInboundReadService(unittest.IsolatedAsyncioTestCase): class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
@@ -774,10 +708,18 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 3, 7])) as scope, patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 3, 7])) as scope,
patch.object(OperationDao, 'get_inbound_list', new=get_list), patch.object(OperationDao, 'get_inbound_list', new=get_list),
): ):
actual = await InboundService.get_inbound_list_services(self.db, query, self.user, is_page=True) actual = await InboundService.get_inbound_list_services(
self.db, query, self.user, TestBatchScope.formal(), is_page=True
)
scope.assert_awaited_once_with(self.db, 7) scope.assert_awaited_once_with(self.db, 7)
get_list.assert_awaited_once_with(self.db, query, True, warehouse_ids=[3, 7]) get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=[3, 7],
scope=TestBatchScope.formal(),
is_page=True,
)
self.assertIsInstance(actual, PageModel) self.assertIsInstance(actual, PageModel)
self.assertIsInstance(actual.rows[0], InboundModel) self.assertIsInstance(actual.rows[0], InboundModel)
@@ -790,10 +732,18 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope), patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
patch.object(OperationDao, 'get_inbound_list', new=get_list), patch.object(OperationDao, 'get_inbound_list', new=get_list),
): ):
await InboundService.get_inbound_list_services(self.db, query, make_user(1), is_page=True) await InboundService.get_inbound_list_services(
self.db, query, make_user(1), TestBatchScope.formal(), is_page=True
)
scope.assert_not_awaited() scope.assert_not_awaited()
get_list.assert_awaited_once_with(self.db, query, True, warehouse_ids=None) get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=None,
scope=TestBatchScope.formal(),
is_page=True,
)
async def test_explicit_unauthorized_warehouse_is_403_before_query(self) -> None: async def test_explicit_unauthorized_warehouse_is_403_before_query(self) -> None:
query = InboundPageQueryModel(warehouseId=9) query = InboundPageQueryModel(warehouseId=9)
@@ -803,7 +753,9 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_list', new=get_list), patch.object(OperationDao, 'get_inbound_list', new=get_list),
self.assertRaises(PermissionException), self.assertRaises(PermissionException),
): ):
await InboundService.get_inbound_list_services(self.db, query, self.user, is_page=True) await InboundService.get_inbound_list_services(
self.db, query, self.user, TestBatchScope.formal(), is_page=True
)
get_list.assert_not_awaited() get_list.assert_not_awaited()
async def test_user_without_scope_gets_empty_paginated_result_without_total_leak(self) -> None: async def test_user_without_scope_gets_empty_paginated_result_without_total_leak(self) -> None:
@@ -814,9 +766,17 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[])),
patch.object(OperationDao, 'get_inbound_list', new=get_list), patch.object(OperationDao, 'get_inbound_list', new=get_list),
): ):
actual = await InboundService.get_inbound_list_services(self.db, query, self.user, is_page=True) actual = await InboundService.get_inbound_list_services(
self.db, query, self.user, TestBatchScope.formal(), is_page=True
)
get_list.assert_awaited_once_with(self.db, query, True, warehouse_ids=[]) get_list.assert_awaited_once_with(
self.db,
query,
warehouse_ids=[],
scope=TestBatchScope.formal(),
is_page=True,
)
self.assertEqual([], actual.rows) self.assertEqual([], actual.rows)
self.assertEqual(0, actual.total) self.assertEqual(0, actual.total)
self.assertFalse(actual.has_next) self.assertFalse(actual.has_next)
@@ -834,10 +794,10 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail), patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
patch.object(AccessService, 'assert_warehouse_access', new=access), patch.object(AccessService, 'assert_warehouse_access', new=access),
): ):
actual = await InboundService.inbound_detail_services(self.db, 11, self.user) actual = await InboundService.inbound_detail_services(self.db, 11, self.user, TestBatchScope.formal())
scope.assert_awaited_once_with(self.db, 7) scope.assert_awaited_once_with(self.db, 7)
get_detail.assert_awaited_once_with(self.db, 11, warehouse_ids=[3, 7]) get_detail.assert_awaited_once_with(self.db, 11, TestBatchScope.formal(), warehouse_ids=[3, 7])
access.assert_not_awaited() access.assert_not_awaited()
self.assertIsInstance(actual, InboundModel) self.assertIsInstance(actual, InboundModel)
@@ -848,11 +808,11 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail), patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
self.assertRaises(Exception) as raised, self.assertRaises(Exception) as raised,
): ):
await InboundService.inbound_detail_services(self.db, 404, self.user) await InboundService.inbound_detail_services(self.db, 404, self.user, TestBatchScope.formal())
self.assertIsInstance(raised.exception, PermissionException) self.assertIsInstance(raised.exception, PermissionException)
self.assertEqual('无权访问该入库单', raised.exception.message) self.assertEqual('无权访问该入库单', raised.exception.message)
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=[7]) get_detail.assert_awaited_once_with(self.db, 404, TestBatchScope.formal(), warehouse_ids=[7])
async def test_admin_missing_detail_keeps_precise_not_found(self) -> None: async def test_admin_missing_detail_keeps_precise_not_found(self) -> None:
scope = AsyncMock() scope = AsyncMock()
@@ -862,11 +822,11 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_by_id', new=get_detail), patch.object(OperationDao, 'get_inbound_by_id', new=get_detail),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await InboundService.inbound_detail_services(self.db, 404, make_user(1)) await InboundService.inbound_detail_services(self.db, 404, make_user(1), TestBatchScope.formal())
self.assertEqual('入库单不存在', raised.exception.message) self.assertEqual('入库单不存在', raised.exception.message)
scope.assert_not_awaited() scope.assert_not_awaited()
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=None) get_detail.assert_awaited_once_with(self.db, 404, TestBatchScope.formal(), warehouse_ids=None)
async def test_warehouse_options_filter_disabled_and_unauthorized_records(self) -> None: async def test_warehouse_options_filter_disabled_and_unauthorized_records(self) -> None:
warehouses = [ warehouses = [
@@ -878,11 +838,12 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)) as list_warehouse, patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)) as list_warehouse,
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[2, 3])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[2, 3])),
): ):
actual = await InboundService.get_warehouse_options_services(self.db, self.user) actual = await InboundService.get_warehouse_options_services(self.db, self.user, TestBatchScope.formal())
query = list_warehouse.await_args.args[1] query = list_warehouse.await_args.args[1]
self.assertEqual('0', query.status) self.assertEqual('0', query.status)
self.assertIs(False, list_warehouse.await_args.args[2]) self.assertEqual(TestBatchScope.formal(), list_warehouse.await_args.args[2])
self.assertIs(False, list_warehouse.await_args.args[3])
self.assertEqual([BusinessOptionModel(id=2, code='W2', name='二仓')], actual) self.assertEqual([BusinessOptionModel(id=2, code='W2', name='二仓')], actual)
self.assertEqual({'id', 'code', 'name'}, set(BusinessOptionModel.model_fields)) self.assertEqual({'id', 'code', 'name'}, set(BusinessOptionModel.model_fields))
@@ -896,7 +857,7 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)), patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)),
patch.object(AccessDao, 'list_user_warehouse_ids', new=scope), patch.object(AccessDao, 'list_user_warehouse_ids', new=scope),
): ):
actual = await InboundService.get_warehouse_options_services(self.db, make_user(1)) actual = await InboundService.get_warehouse_options_services(self.db, make_user(1), TestBatchScope.formal())
scope.assert_not_awaited() scope.assert_not_awaited()
self.assertEqual([2, 5], [option.id for option in actual]) self.assertEqual([2, 5], [option.id for option in actual])
@@ -914,7 +875,7 @@ class TestInboundReadService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)), patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)),
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[2])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[2])),
): ):
actual = await InboundService.get_warehouse_options_services(self.db, self.user) actual = await InboundService.get_warehouse_options_services(self.db, self.user, TestBatchScope.formal())
self.assertEqual([BusinessOptionModel(id=2, code='W2', name='二仓')], actual) self.assertEqual([BusinessOptionModel(id=2, code='W2', name='二仓')], actual)
@@ -945,11 +906,12 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
'persisted-token', 'persisted-token',
self.query, self.query,
self.user, self.user,
TestBatchScope.formal(),
) )
access.assert_awaited_once_with(self.db, self.user, 7) access.assert_awaited_once_with(self.db, self.user, 7)
get_pallet.assert_awaited_once_with(self.db, 'persisted-token') get_pallet.assert_awaited_once_with(self.db, 'persisted-token', TestBatchScope.formal())
get_inventory.assert_awaited_once_with(self.db, 2) get_inventory.assert_awaited_once_with(self.db, 2, TestBatchScope.formal())
locking_read.assert_not_awaited() locking_read.assert_not_awaited()
add_event.assert_not_awaited() add_event.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -987,6 +949,7 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
'persisted-token', 'persisted-token',
self.query, self.query,
self.user, self.user,
TestBatchScope.formal(),
) )
async def test_scan_rejects_unauthorized_disabled_or_missing_target_warehouse_first(self) -> None: async def test_scan_rejects_unauthorized_disabled_or_missing_target_warehouse_first(self) -> None:
@@ -1001,7 +964,9 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=get_pallet), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=get_pallet),
self.assertRaises(PermissionException), self.assertRaises(PermissionException),
): ):
await InboundService.scan_inbound_pallet_services(self.db, 'token', self.query, self.user) await InboundService.scan_inbound_pallet_services(
self.db, 'token', self.query, self.user, TestBatchScope.formal()
)
get_pallet.assert_not_awaited() get_pallet.assert_not_awaited()
for warehouse in (None, SimpleNamespace(warehouse_id=7, status='1')): for warehouse in (None, SimpleNamespace(warehouse_id=7, status='1')):
@@ -1013,7 +978,9 @@ class TestInboundScanService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=get_pallet), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=get_pallet),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.scan_inbound_pallet_services(self.db, 'token', self.query, self.user) await InboundService.scan_inbound_pallet_services(
self.db, 'token', self.query, self.user, TestBatchScope.formal()
)
get_pallet.assert_not_awaited() get_pallet.assert_not_awaited()
@@ -1045,12 +1012,14 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock), patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
self.assertRaises(Exception) as raised, self.assertRaises(Exception) as raised,
): ):
await InboundService.confirm_inbound_services(self.db, 404, self.command, self.user) await InboundService.confirm_inbound_services(
self.db, 404, self.command, self.user, TestBatchScope.formal()
)
self.assertIsInstance(raised.exception, PermissionException) self.assertIsInstance(raised.exception, PermissionException)
self.assertEqual('无权访问该入库单', raised.exception.message) self.assertEqual('无权访问该入库单', raised.exception.message)
scope.assert_awaited_once_with(self.db, 7) scope.assert_awaited_once_with(self.db, 7)
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=[7]) get_detail.assert_awaited_once_with(self.db, 404, TestBatchScope.formal(), warehouse_ids=[7])
get_by_key.assert_not_awaited() get_by_key.assert_not_awaited()
lock_order.assert_not_awaited() lock_order.assert_not_awaited()
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
@@ -1068,11 +1037,13 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_for_update', new=lock_order), patch.object(OperationDao, 'get_inbound_for_update', new=lock_order),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await InboundService.confirm_inbound_services(self.db, 404, self.command, make_user(1)) await InboundService.confirm_inbound_services(
self.db, 404, self.command, make_user(1), TestBatchScope.formal()
)
self.assertEqual('入库单不存在', raised.exception.message) self.assertEqual('入库单不存在', raised.exception.message)
scope.assert_not_awaited() scope.assert_not_awaited()
get_detail.assert_awaited_once_with(self.db, 404, warehouse_ids=None) get_detail.assert_awaited_once_with(self.db, 404, TestBatchScope.formal(), warehouse_ids=None)
get_by_key.assert_not_awaited() get_by_key.assert_not_awaited()
lock_order.assert_not_awaited() lock_order.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -1169,7 +1140,9 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patches[10] as warehouse_lock, patches[10] as warehouse_lock,
): ):
service_datetime.now.return_value = self.fixed_time service_datetime.now.return_value = self.fixed_time
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) result = await InboundService.confirm_inbound_services(
self.db, 11, self.command, self.user, TestBatchScope.formal()
)
return { return {
'result': result, 'result': result,
@@ -1209,17 +1182,33 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
captured['actions'], captured['actions'],
) )
self.default_scope.assert_awaited_once_with(self.db, 7) self.default_scope.assert_awaited_once_with(self.db, 7)
captured['detail_read'].assert_awaited_once_with(self.db, 11, warehouse_ids=[7]) captured['detail_read'].assert_awaited_once_with(
self.db,
11,
TestBatchScope.formal(),
warehouse_ids=[7],
)
captured['key_read'].assert_awaited_once_with( captured['key_read'].assert_awaited_once_with(
self.db, self.db,
'confirm-key', 'confirm-key',
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
) )
captured['order_lock'].assert_awaited_once_with(self.db, 11, warehouse_ids=[7]) captured['order_lock'].assert_awaited_once_with(
captured['warehouse_lock'].assert_awaited_once_with(self.db, 7, for_update=True) self.db,
11,
TestBatchScope.formal(),
warehouse_ids=[7],
)
captured['warehouse_lock'].assert_awaited_once_with(
self.db,
7,
TestBatchScope.formal(),
for_update=True,
)
captured['access_probe'].assert_awaited_once_with(self.db, self.user, 7, for_update=True) captured['access_probe'].assert_awaited_once_with(self.db, self.user, 7, for_update=True)
captured['inventory_lock'].assert_awaited_once_with(self.db, [2, 5]) captured['inventory_lock'].assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
captured['pallet_lock'].assert_awaited_once_with(self.db, [2, 5]) captured['pallet_lock'].assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
captured['ensure_transition'].assert_has_calls( captured['ensure_transition'].assert_has_calls(
[ [
unittest.mock.call(PalletFlowState.UNSTOCKED, PalletFlowState.EMPTY_IN_WAREHOUSE), unittest.mock.call(PalletFlowState.UNSTOCKED, PalletFlowState.EMPTY_IN_WAREHOUSE),
@@ -1299,10 +1288,10 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该入库单', raised.exception.message) self.assertEqual('无权访问该入库单', raised.exception.message)
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_lock.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
warehouse_mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True) warehouse_mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True)
self.assertEqual('DRAFT', inbound.status) self.assertEqual('DRAFT', inbound.status)
self.assertEqual('UNSTOCKED', inventory.flow_state) self.assertEqual('UNSTOCKED', inventory.flow_state)
@@ -1328,9 +1317,9 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
pallet_lock.assert_awaited_once_with(self.db, [2, 5]) pallet_lock.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
self.assertEqual('DRAFT', inbound.status) self.assertEqual('DRAFT', inbound.status)
self.assertEqual('UNSTOCKED', inventories[0].flow_state) self.assertEqual('UNSTOCKED', inventories[0].flow_state)
update.assert_not_awaited() update.assert_not_awaited()
@@ -1360,7 +1349,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaisesRegex(ValueError, 'invalid'), self.assertRaisesRegex(ValueError, 'invalid'),
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
self.assertEqual('DRAFT', inbound.status) self.assertEqual('DRAFT', inbound.status)
self.assertEqual('UNSTOCKED', inventory.flow_state) self.assertEqual('UNSTOCKED', inventory.flow_state)
@@ -1391,10 +1380,12 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')), new=AsyncMock(return_value=SimpleNamespace(warehouse_id=7, status='0')),
) as warehouse_lock, ) as warehouse_lock,
): ):
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) result = await InboundService.confirm_inbound_services(
self.db, 11, self.command, self.user, TestBatchScope.formal()
)
lock.assert_awaited_once_with(self.db, 11, warehouse_ids=[7]) lock.assert_awaited_once_with(self.db, 11, TestBatchScope.formal(), warehouse_ids=[7])
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_lock.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
self.assertEqual( self.assertEqual(
[ [
unittest.mock.call(self.db, self.user, 7), unittest.mock.call(self.db, self.user, 7),
@@ -1424,6 +1415,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
async def read_key( async def read_key(
_db: AsyncSession, _db: AsyncSession,
_key: str, _key: str,
_scope: TestBatchScope,
*, *,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None: ) -> BizInboundOrder | None:
@@ -1455,13 +1447,15 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
11, 11,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
get_detail.assert_awaited_once_with(self.db, 11, warehouse_ids=[7, 8]) get_detail.assert_awaited_once_with(self.db, 11, TestBatchScope.formal(), warehouse_ids=[7, 8])
key_read.assert_awaited_once_with( key_read.assert_awaited_once_with(
self.db, self.db,
'confirm-key', 'confirm-key',
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
) )
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -1496,10 +1490,10 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping), patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该入库单', raised.exception.message) self.assertEqual('无权访问该入库单', raised.exception.message)
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_lock.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True) warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -1526,7 +1520,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_inbound_for_update', new=lock), patch.object(OperationDao, 'get_inbound_for_update', new=lock),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
access.assert_awaited_once_with(self.db, self.user, 8) access.assert_awaited_once_with(self.db, self.user, 8)
lock.assert_not_awaited() lock.assert_not_awaited()
@@ -1548,7 +1542,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock), patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_lock),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -1574,10 +1568,12 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'update_inbound', new=AsyncMock()), patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
patch.object(InventoryDao, 'add_event', new=AsyncMock()), patch.object(InventoryDao, 'add_event', new=AsyncMock()),
): ):
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) result = await InboundService.confirm_inbound_services(
self.db, 11, self.command, self.user, TestBatchScope.formal()
)
get_detail.assert_awaited_once_with(self.db, 11, warehouse_ids=[7]) get_detail.assert_awaited_once_with(self.db, 11, TestBatchScope.formal(), warehouse_ids=[7])
inventory_lock.assert_awaited_once_with(self.db, [5]) inventory_lock.assert_awaited_once_with(self.db, [5], TestBatchScope.formal())
self.assertEqual('EMPTY_IN_WAREHOUSE', inventory.flow_state) self.assertEqual('EMPTY_IN_WAREHOUSE', inventory.flow_state)
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
@@ -1608,7 +1604,9 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'update_inbound', new=AsyncMock()), patch.object(OperationDao, 'update_inbound', new=AsyncMock()),
patch.object(InventoryDao, 'add_event', new=AsyncMock()), patch.object(InventoryDao, 'add_event', new=AsyncMock()),
): ):
result = await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) result = await InboundService.confirm_inbound_services(
self.db, 11, self.command, self.user, TestBatchScope.formal()
)
self.assertEqual(2, parent_lock.await_count) self.assertEqual(2, parent_lock.await_count)
self.assertEqual( self.assertEqual(
@@ -1621,8 +1619,8 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
self.assertEqual(2, key_reads.await_count) self.assertEqual(2, key_reads.await_count)
key_reads.assert_has_awaits( key_reads.assert_has_awaits(
[ [
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]), unittest.mock.call(self.db, 'confirm-key', TestBatchScope.formal(), warehouse_ids=[7]),
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]), unittest.mock.call(self.db, 'confirm-key', TestBatchScope.formal(), warehouse_ids=[7]),
] ]
) )
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -1647,6 +1645,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
async def read_key( async def read_key(
_db: AsyncSession, _db: AsyncSession,
_key: str, _key: str,
_scope: TestBatchScope,
*, *,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
) -> BizInboundOrder | None: ) -> BizInboundOrder | None:
@@ -1683,6 +1682,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
11, 11,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
@@ -1690,8 +1690,8 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
self.assertEqual(2, parent_lock.await_count) self.assertEqual(2, parent_lock.await_count)
self.assertEqual( self.assertEqual(
[ [
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]), unittest.mock.call(self.db, 'confirm-key', TestBatchScope.formal(), warehouse_ids=[7]),
unittest.mock.call(self.db, 'confirm-key', warehouse_ids=[7]), unittest.mock.call(self.db, 'confirm-key', TestBatchScope.formal(), warehouse_ids=[7]),
], ],
key_read.await_args_list, key_read.await_args_list,
) )
@@ -1743,11 +1743,11 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=AsyncMock()), patch.object(InventoryDao, 'add_event', new=AsyncMock()),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该入库单', raised.exception.message) self.assertEqual('无权访问该入库单', raised.exception.message)
self.assertEqual(2, parent_lock.await_count) self.assertEqual(2, parent_lock.await_count)
parent_lock.assert_awaited_with(self.db, 11, warehouse_ids=[7]) parent_lock.assert_awaited_with(self.db, 11, TestBatchScope.formal(), warehouse_ids=[7])
self.assertEqual(2, warehouse_lock.await_count) self.assertEqual(2, warehouse_lock.await_count)
self.assertEqual(2, locked_access_count) self.assertEqual(2, locked_access_count)
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -1782,7 +1782,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=AsyncMock()), patch.object(InventoryDao, 'add_event', new=AsyncMock()),
self.assertRaises(ServiceException), self.assertRaises(ServiceException),
): ):
await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user) await InboundService.confirm_inbound_services(self.db, 11, self.command, self.user, TestBatchScope.formal())
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -1836,6 +1836,7 @@ class TestInboundLogPermission(unittest.IsolatedAsyncioTestCase):
add_inbound=InboundCreateModel(warehouseId=404, palletIds=[2]), add_inbound=InboundCreateModel(warehouseId=404, palletIds=[2]),
query_db=db, query_db=db,
current_user=user, current_user=user,
test_scope=TestBatchScope.formal(),
) )
finally: finally:
RequestContext.reset_current_user(user_token) RequestContext.reset_current_user(user_token)
@@ -13,6 +13,7 @@ from module_business.dao import inventory_dao as inventory_dao_module
from module_business.dao import operation_dao as operation_dao_module from module_business.dao import operation_dao as operation_dao_module
from module_business.dao.inventory_dao import InventoryDao from module_business.dao.inventory_dao import InventoryDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizPallet from module_business.entity.do.master_do import BizPallet
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
@@ -66,11 +67,11 @@ class TestDaoPublicContract(unittest.TestCase):
def test_inventory_dao_critical_parameter_names_are_stable(self) -> None: def test_inventory_dao_critical_parameter_names_are_stable(self) -> None:
expected = { expected = {
'get_inventory_for_update': ('db', 'pallet_id'), 'get_inventory_for_update': ('db', 'pallet_id', 'scope'),
'get_inventories_for_update': ('db', 'pallet_ids'), 'get_inventories_for_update': ('db', 'pallet_ids', 'scope'),
'get_pallet_by_qr_token': ('db', 'qr_token'), 'get_pallet_by_qr_token': ('db', 'qr_token', 'scope'),
'get_pallets_by_ids': ('db', 'pallet_ids'), 'get_pallets_by_ids': ('db', 'pallet_ids', 'scope'),
'get_event_by_idempotency_key': ('db', 'pallet_id', 'idempotency_key'), 'get_event_by_idempotency_key': ('db', 'pallet_id', 'idempotency_key', 'scope'),
'add_event': ('db', 'inventory', 'event'), 'add_event': ('db', 'inventory', 'event'),
} }
for method_name, parameter_names in expected.items(): for method_name, parameter_names in expected.items():
@@ -121,51 +122,55 @@ class TestDaoPublicContract(unittest.TestCase):
def test_operation_dao_critical_parameter_names_and_defaults_are_stable(self) -> None: def test_operation_dao_critical_parameter_names_and_defaults_are_stable(self) -> None:
expected = { expected = {
'get_inbound_for_update': ('db', 'inbound_id', 'warehouse_ids'), 'get_inbound_for_update': ('db', 'inbound_id', 'scope', 'warehouse_ids'),
'get_shipment_for_update': ('db', 'shipment_id', 'warehouse_ids', 'site_ids'), 'get_shipment_for_update': ('db', 'shipment_id', 'scope', 'warehouse_ids', 'site_ids'),
'get_return_for_update': ('db', 'return_id', 'site_ids', 'warehouse_ids'), 'get_return_for_update': ('db', 'return_id', 'scope', 'site_ids', 'warehouse_ids'),
'get_active_filling_for_update': ('db', 'filling_id'), 'get_active_filling_for_update': ('db', 'filling_id', 'scope'),
'get_inbound_by_confirm_idempotency_key': ( 'get_inbound_by_confirm_idempotency_key': (
'db', 'db',
'idempotency_key', 'idempotency_key',
'scope',
'warehouse_ids', 'warehouse_ids',
), ),
'get_shipment_by_confirm_idempotency_key': ( 'get_shipment_by_confirm_idempotency_key': (
'db', 'db',
'idempotency_key', 'idempotency_key',
'scope',
'warehouse_ids', 'warehouse_ids',
'site_ids', 'site_ids',
), ),
'get_filling_by_idempotency_key': ('db', 'idempotency_key', 'warehouse_ids'), 'get_filling_by_idempotency_key': ('db', 'idempotency_key', 'scope', 'warehouse_ids'),
'get_usage_by_idempotency_key': ('db', 'idempotency_key', 'site_ids'), 'get_usage_by_idempotency_key': ('db', 'idempotency_key', 'scope', 'site_ids'),
'get_return_by_request_idempotency_key': ( 'get_return_by_request_idempotency_key': (
'db', 'db',
'idempotency_key', 'idempotency_key',
'source_site_id', 'source_site_id',
'target_warehouse_id', 'target_warehouse_id',
'scope',
), ),
'get_return_by_confirm_idempotency_key': ( 'get_return_by_confirm_idempotency_key': (
'db', 'db',
'idempotency_key', 'idempotency_key',
'source_site_id', 'source_site_id',
'target_warehouse_id', 'target_warehouse_id',
'scope',
), ),
'get_inbound_by_id': ('db', 'inbound_id', 'warehouse_ids'), 'get_inbound_by_id': ('db', 'inbound_id', 'scope', 'warehouse_ids'),
'add_inbound': ('db', 'inbound'), 'add_inbound': ('db', 'inbound'),
'update_inbound': ('db', 'inbound'), 'update_inbound': ('db', 'inbound'),
'delete_inbound': ('db', 'inbound_id'), 'delete_inbound': ('db', 'inbound_id'),
'replace_inbound_items': ('db', 'inbound_id', 'items'), 'replace_inbound_items': ('db', 'inbound_id', 'items'),
'get_filling_by_id': ('db', 'filling_id', 'warehouse_ids'), 'get_filling_by_id': ('db', 'filling_id', 'scope', 'warehouse_ids'),
'add_filling_record': ('db', 'filling'), 'add_filling_record': ('db', 'filling'),
'update_filling_record': ('db', 'filling'), 'update_filling_record': ('db', 'filling'),
'get_shipment_by_id': ('db', 'shipment_id', 'warehouse_ids', 'site_ids'), 'get_shipment_by_id': ('db', 'shipment_id', 'scope', 'warehouse_ids', 'site_ids'),
'add_shipment': ('db', 'shipment'), 'add_shipment': ('db', 'shipment'),
'update_shipment': ('db', 'shipment'), 'update_shipment': ('db', 'shipment'),
'delete_shipment': ('db', 'shipment_id'), 'delete_shipment': ('db', 'shipment_id'),
'replace_shipment_items': ('db', 'shipment_id', 'items'), 'replace_shipment_items': ('db', 'shipment_id', 'items'),
'get_usage_by_id': ('db', 'usage_id', 'site_ids'), 'get_usage_by_id': ('db', 'usage_id', 'scope', 'site_ids'),
'add_usage_record': ('db', 'usage'), 'add_usage_record': ('db', 'usage'),
'get_return_by_id': ('db', 'return_id', 'site_ids', 'warehouse_ids'), 'get_return_by_id': ('db', 'return_id', 'scope', 'site_ids', 'warehouse_ids'),
'add_return_order': ('db', 'return_order'), 'add_return_order': ('db', 'return_order'),
'update_return_order': ('db', 'return_order'), 'update_return_order': ('db', 'return_order'),
'add_return_items': ('db', 'items'), 'add_return_items': ('db', 'items'),
@@ -177,11 +182,10 @@ class TestDaoPublicContract(unittest.TestCase):
inbound_list_signature = inspect.signature(OperationDao.get_inbound_list) inbound_list_signature = inspect.signature(OperationDao.get_inbound_list)
self.assertEqual( self.assertEqual(
('db', 'query_object', 'is_page', 'warehouse_ids'), ('db', 'query_object', 'warehouse_ids', 'scope', 'is_page'),
tuple(inbound_list_signature.parameters), tuple(inbound_list_signature.parameters),
) )
self.assertIs(True, inbound_list_signature.parameters['is_page'].default) self.assertIs(True, inbound_list_signature.parameters['is_page'].default)
self.assertIsNone(inbound_list_signature.parameters['warehouse_ids'].default)
for method_name in ( for method_name in (
'get_inbound_for_update', 'get_inbound_for_update',
@@ -208,28 +212,24 @@ class TestDaoPublicContract(unittest.TestCase):
filling_list_signature = inspect.signature(OperationDao.get_filling_list) filling_list_signature = inspect.signature(OperationDao.get_filling_list)
self.assertEqual( self.assertEqual(
('db', 'query_object', 'is_page', 'warehouse_ids'), ('db', 'query_object', 'warehouse_ids', 'scope', 'is_page'),
tuple(filling_list_signature.parameters), tuple(filling_list_signature.parameters),
) )
self.assertIs(True, filling_list_signature.parameters['is_page'].default) self.assertIs(True, filling_list_signature.parameters['is_page'].default)
self.assertIsNone(filling_list_signature.parameters['warehouse_ids'].default)
shipment_list_signature = inspect.signature(OperationDao.get_shipment_list) shipment_list_signature = inspect.signature(OperationDao.get_shipment_list)
self.assertEqual( self.assertEqual(
('db', 'query_object', 'is_page', 'warehouse_ids', 'site_ids'), ('db', 'query_object', 'warehouse_ids', 'site_ids', 'scope', 'is_page'),
tuple(shipment_list_signature.parameters), tuple(shipment_list_signature.parameters),
) )
self.assertIs(True, shipment_list_signature.parameters['is_page'].default) 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)
usage_list_signature = inspect.signature(OperationDao.get_usage_list) usage_list_signature = inspect.signature(OperationDao.get_usage_list)
self.assertEqual( self.assertEqual(
('db', 'query_object', 'is_page', 'site_ids'), ('db', 'query_object', 'site_ids', 'scope', 'is_page'),
tuple(usage_list_signature.parameters), tuple(usage_list_signature.parameters),
) )
self.assertIs(True, usage_list_signature.parameters['is_page'].default) self.assertIs(True, usage_list_signature.parameters['is_page'].default)
self.assertIsNone(usage_list_signature.parameters['site_ids'].default)
for method_name in ( for method_name in (
'get_usage_by_idempotency_key', 'get_usage_by_idempotency_key',
@@ -243,14 +243,13 @@ class TestDaoPublicContract(unittest.TestCase):
return_list_signature = inspect.signature(OperationDao.get_return_list) return_list_signature = inspect.signature(OperationDao.get_return_list)
self.assertEqual( self.assertEqual(
('db', 'query_object', 'is_page', 'site_ids', 'warehouse_ids'), ('db', 'query_object', 'site_ids', 'warehouse_ids', 'scope', 'is_page'),
tuple(return_list_signature.parameters), tuple(return_list_signature.parameters),
) )
self.assertIs(True, return_list_signature.parameters['is_page'].default) self.assertIs(True, return_list_signature.parameters['is_page'].default)
for method_name in ( for method_name in (
'get_return_for_update', 'get_return_for_update',
'get_return_by_id', 'get_return_by_id',
'get_return_list',
): ):
with self.subTest(method=method_name): with self.subTest(method=method_name):
signature = inspect.signature(getattr(OperationDao, method_name)) signature = inspect.signature(getattr(OperationDao, method_name))
@@ -284,7 +283,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = inventory result.scalar_one_or_none.return_value = inventory
self.db.execute.return_value = result self.db.execute.return_value = result
actual = await InventoryDao.get_inventory_for_update(self.db, pallet_id=7) actual = await InventoryDao.get_inventory_for_update(self.db, pallet_id=7, scope=TestBatchScope.formal())
self.assertIs(inventory, actual) self.assertIs(inventory, actual)
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -305,7 +304,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
result.scalars.return_value.all.return_value = inventories result.scalars.return_value.all.return_value = inventories
self.db.execute.return_value = result self.db.execute.return_value = result
actual = await InventoryDao.get_inventories_for_update(self.db, [9, 2, 5, 2]) actual = await InventoryDao.get_inventories_for_update(self.db, [9, 2, 5, 2], scope=TestBatchScope.formal())
self.assertEqual([2, 5, 9], [inventory.pallet_id for inventory in actual]) self.assertEqual([2, 5, 9], [inventory.pallet_id for inventory in actual])
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -318,7 +317,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
self.assertIn('FOR UPDATE', sql) self.assertIn('FOR UPDATE', sql)
async def test_get_inventories_for_update_empty_input_skips_sql(self) -> None: async def test_get_inventories_for_update_empty_input_skips_sql(self) -> None:
self.assertEqual([], await InventoryDao.get_inventories_for_update(self.db, [])) self.assertEqual([], await InventoryDao.get_inventories_for_update(self.db, [], scope=TestBatchScope.formal()))
self.db.execute.assert_not_awaited() self.db.execute.assert_not_awaited()
async def test_get_pallet_by_qr_token_uses_exact_token_without_lock(self) -> None: async def test_get_pallet_by_qr_token_uses_exact_token_without_lock(self) -> None:
@@ -327,7 +326,9 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = pallet result.scalar_one_or_none.return_value = pallet
self.db.execute.return_value = result self.db.execute.return_value = result
actual = await InventoryDao.get_pallet_by_qr_token(self.db, qr_token='fixed-token') actual = await InventoryDao.get_pallet_by_qr_token(
self.db, qr_token='fixed-token', scope=TestBatchScope.formal()
)
self.assertIs(pallet, actual) self.assertIs(pallet, actual)
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -346,7 +347,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
result.scalars.return_value.all.return_value = pallets result.scalars.return_value.all.return_value = pallets
self.db.execute.return_value = result self.db.execute.return_value = result
actual = await InventoryDao.get_pallets_by_ids(self.db, [9, 2, 5, 2]) actual = await InventoryDao.get_pallets_by_ids(self.db, [9, 2, 5, 2], scope=TestBatchScope.formal())
self.assertEqual([2, 5, 9], [pallet.pallet_id for pallet in actual]) self.assertEqual([2, 5, 9], [pallet.pallet_id for pallet in actual])
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -359,7 +360,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
self.assertIn('FOR UPDATE', sql) self.assertIn('FOR UPDATE', sql)
async def test_get_pallets_by_ids_empty_input_skips_sql(self) -> None: async def test_get_pallets_by_ids_empty_input_skips_sql(self) -> None:
self.assertEqual([], await InventoryDao.get_pallets_by_ids(self.db, [])) self.assertEqual([], await InventoryDao.get_pallets_by_ids(self.db, [], scope=TestBatchScope.formal()))
self.db.execute.assert_not_awaited() self.db.execute.assert_not_awaited()
async def test_get_event_by_idempotency_key_uses_pallet_scoped_key(self) -> None: async def test_get_event_by_idempotency_key_uses_pallet_scoped_key(self) -> None:
@@ -372,6 +373,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
pallet_id=7, pallet_id=7,
idempotency_key='event-key', idempotency_key='event-key',
scope=TestBatchScope.formal(),
) )
self.assertIs(event, actual) self.assertIs(event, actual)
@@ -474,7 +476,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = record result.scalar_one_or_none.return_value = record
self.db.execute.return_value = result self.db.execute.return_value = result
actual = await method(self.db, record_id) actual = await method(self.db, record_id, scope=TestBatchScope.formal())
self.assertIs(record, actual) self.assertIs(record, actual)
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -504,6 +506,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
12, 12,
warehouse_ids=[7, 3, 7], warehouse_ids=[7, 3, 7],
site_ids=[11, 9, 11], site_ids=[11, 9, 11],
scope=TestBatchScope.formal(),
) )
self.assertIs(shipment, actual) self.assertIs(shipment, actual)
@@ -540,6 +543,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
12, 12,
warehouse_ids=[], warehouse_ids=[],
site_ids=[9], site_ids=[9],
scope=TestBatchScope.formal(),
) )
self.assertIsNone(actual) self.assertIsNone(actual)
@@ -562,7 +566,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
item_result.scalars.return_value.all.return_value = items item_result.scalars.return_value.all.return_value = items
self.db.execute.side_effect = [inbound_result, item_result] self.db.execute.side_effect = [inbound_result, item_result]
actual = await OperationDao.get_inbound_for_update(self.db, inbound_id=11) actual = await OperationDao.get_inbound_for_update(self.db, inbound_id=11, scope=TestBatchScope.formal())
self.assertIs(inbound, actual) self.assertIs(inbound, actual)
self.assertEqual(items, actual.items) self.assertEqual(items, actual.items)
@@ -599,6 +603,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
inbound_id=11, inbound_id=11,
warehouse_ids=[7, 3, 7], warehouse_ids=[7, 3, 7],
scope=TestBatchScope.formal(),
) )
self.assertIsNone(actual) self.assertIsNone(actual)
@@ -647,7 +652,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = record result.scalar_one_or_none.return_value = record
self.db.execute.return_value = result self.db.execute.return_value = result
actual = await method(self.db, 'same-key', **kwargs) actual = await method(self.db, 'same-key', scope=TestBatchScope.formal(), **kwargs)
self.assertIs(record, actual) self.assertIs(record, actual)
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -673,6 +678,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
'confirm-key', 'confirm-key',
warehouse_ids=[7, 3, 7], warehouse_ids=[7, 3, 7],
scope=TestBatchScope.formal(),
) )
self.assertIsNone(actual) self.assertIsNone(actual)
@@ -807,7 +813,16 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
marker = object() marker = object()
is_page = index % 2 == 0 is_page = index % 2 == 0
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as paginate: with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as paginate:
actual = await method(self.db, query, is_page=is_page) kwargs = {
'scope': TestBatchScope.formal(),
'is_page': is_page,
}
parameters = inspect.signature(method).parameters
if 'warehouse_ids' in parameters:
kwargs['warehouse_ids'] = None
if 'site_ids' in parameters:
kwargs['site_ids'] = None
actual = await method(self.db, query, **kwargs)
self.assertIs(marker, actual) self.assertIs(marker, actual)
args = paginate.await_args.args args = paginate.await_args.args
@@ -829,6 +844,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
query, query,
is_page=True, is_page=True,
warehouse_ids=[5, 2, 5], warehouse_ids=[5, 2, 5],
scope=TestBatchScope.formal(),
) )
self.assertIs(marker, actual) self.assertIs(marker, actual)
@@ -848,6 +864,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
query, query,
is_page=True, is_page=True,
warehouse_ids=[], warehouse_ids=[],
scope=TestBatchScope.formal(),
) )
self.assertIs(marker, actual) self.assertIs(marker, actual)
@@ -866,6 +883,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
query, query,
is_page=True, is_page=True,
warehouse_ids=None, warehouse_ids=None,
scope=TestBatchScope.formal(),
) )
sql = compile_sql(paginate.await_args.args[1]) sql = compile_sql(paginate.await_args.args[1])
@@ -882,6 +900,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
is_page=True, is_page=True,
warehouse_ids=[7, 3, 7], warehouse_ids=[7, 3, 7],
site_ids=[11, 9, 11], site_ids=[11, 9, 11],
scope=TestBatchScope.formal(),
) )
self.assertIs(marker, actual) self.assertIs(marker, actual)
@@ -898,6 +917,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
query, query,
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[], site_ids=[],
scope=TestBatchScope.formal(),
) )
empty_statement = empty_paginate.await_args.args[1] empty_statement = empty_paginate.await_args.args[1]
self.assertIn('false = 1', compile_sql(empty_statement, mysql.dialect())) self.assertIn('false = 1', compile_sql(empty_statement, mysql.dialect()))
@@ -914,6 +934,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
12, 12,
warehouse_ids=[7, 3], warehouse_ids=[7, 3],
site_ids=[11, 9], site_ids=[11, 9],
scope=TestBatchScope.formal(),
) )
) )
self.db.execute.assert_awaited_once() self.db.execute.assert_awaited_once()
@@ -927,6 +948,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
'confirm-key', 'confirm-key',
warehouse_ids=[7, 3], warehouse_ids=[7, 3],
site_ids=[11, 9], site_ids=[11, 9],
scope=TestBatchScope.formal(),
) )
) )
key_statement = self.db.execute.await_args.args[0] key_statement = self.db.execute.await_args.args[0]
@@ -949,6 +971,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
query, query,
is_page=True, is_page=True,
site_ids=[11, 9, 11], site_ids=[11, 9, 11],
scope=TestBatchScope.formal(),
) )
self.assertIs(marker, actual) self.assertIs(marker, actual)
@@ -957,7 +980,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
scalar_result = MagicMock() scalar_result = MagicMock()
scalar_result.scalar_one_or_none.return_value = None scalar_result.scalar_one_or_none.return_value = None
self.db.execute.return_value = scalar_result self.db.execute.return_value = scalar_result
await OperationDao.get_usage_by_id(self.db, 15, site_ids=[11, 9, 11]) await OperationDao.get_usage_by_id(self.db, 15, site_ids=[11, 9, 11], scope=TestBatchScope.formal())
detail_statement = self.db.execute.await_args.args[0] detail_statement = self.db.execute.await_args.args[0]
self.db.reset_mock() self.db.reset_mock()
@@ -966,6 +989,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
'usage-key', 'usage-key',
site_ids=[11, 9, 11], site_ids=[11, 9, 11],
scope=TestBatchScope.formal(),
) )
key_statement = self.db.execute.await_args.args[0] key_statement = self.db.execute.await_args.args[0]
@@ -976,17 +1000,19 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertIn('biz_usage_record.site_id IN (9, 11)', sql) self.assertIn('biz_usage_record.site_id IN (9, 11)', sql)
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as empty_paginate: with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as empty_paginate:
await OperationDao.get_usage_list(self.db, query, site_ids=[]) await OperationDao.get_usage_list(self.db, query, site_ids=[], scope=TestBatchScope.formal())
empty_list = empty_paginate.await_args.args[1] empty_list = empty_paginate.await_args.args[1]
self.db.reset_mock() self.db.reset_mock()
self.db.execute.return_value = scalar_result self.db.execute.return_value = scalar_result
await OperationDao.get_usage_by_id(self.db, 15, site_ids=[]) await OperationDao.get_usage_by_id(self.db, 15, site_ids=[], scope=TestBatchScope.formal())
empty_detail = self.db.execute.await_args.args[0] empty_detail = self.db.execute.await_args.args[0]
self.db.reset_mock() self.db.reset_mock()
self.db.execute.return_value = scalar_result self.db.execute.return_value = scalar_result
await OperationDao.get_usage_by_idempotency_key(self.db, 'usage-key', site_ids=[]) await OperationDao.get_usage_by_idempotency_key(
self.db, 'usage-key', site_ids=[], scope=TestBatchScope.formal()
)
empty_key = self.db.execute.await_args.args[0] empty_key = self.db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()): for dialect in (mysql.dialect(), postgresql.dialect()):
@@ -999,7 +1025,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
query = UsagePageQueryModel() query = UsagePageQueryModel()
marker = object() marker = object()
with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as paginate: with patch.object(PageUtil, 'paginate', new=AsyncMock(return_value=marker)) as paginate:
await OperationDao.get_usage_list(self.db, query, site_ids=None) await OperationDao.get_usage_list(self.db, query, site_ids=None, scope=TestBatchScope.formal())
sql = compile_sql(paginate.await_args.args[1]) sql = compile_sql(paginate.await_args.args[1])
self.assertNotIn('site_id IN', sql) self.assertNotIn('site_id IN', sql)
@@ -1023,6 +1049,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
'usage-key', 'usage-key',
site_ids=site_ids, site_ids=site_ids,
scope=TestBatchScope.formal(),
) )
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -1081,7 +1108,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
item_result.scalars.return_value.all.return_value = items item_result.scalars.return_value.all.return_value = items
self.db.execute.side_effect = [order_result, item_result] self.db.execute.side_effect = [order_result, item_result]
actual = await method(self.db, order_id) actual = await method(self.db, order_id, scope=TestBatchScope.formal())
self.assertIs(order, actual) self.assertIs(order, actual)
self.assertEqual(items, order.items) self.assertEqual(items, order.items)
@@ -1102,7 +1129,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = None result.scalar_one_or_none.return_value = None
self.db.execute.return_value = result self.db.execute.return_value = result
self.assertIsNone(await OperationDao.get_inbound_by_id(self.db, inbound_id=404)) self.assertIsNone(await OperationDao.get_inbound_by_id(self.db, inbound_id=404, scope=TestBatchScope.formal()))
self.db.execute.assert_awaited_once() self.db.execute.assert_awaited_once()
@@ -1119,6 +1146,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
inbound_id=11, inbound_id=11,
warehouse_ids=[], warehouse_ids=[],
scope=TestBatchScope.formal(),
) )
self.assertIsNone(actual) self.assertIsNone(actual)
@@ -1142,7 +1170,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
result.scalar_one_or_none.return_value = record result.scalar_one_or_none.return_value = record
self.db.execute.return_value = result self.db.execute.return_value = result
actual = await method(self.db, record_id) actual = await method(self.db, record_id, scope=TestBatchScope.formal())
self.assertIs(record, actual) self.assertIs(record, actual)
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -25,6 +25,7 @@ from module_business.dao.inventory_dao import InventoryDao
from module_business.dao.master_dao import MasterDao from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizWarehouse from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizWarehouse
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
@@ -184,20 +185,22 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
def test_return_reads_expose_stage_scopes(self) -> None: def test_return_reads_expose_stage_scopes(self) -> None:
expected = { expected = {
OperationDao.get_return_for_update: ('db', 'return_id', 'site_ids', 'warehouse_ids'), OperationDao.get_return_for_update: ('db', 'return_id', 'scope', 'site_ids', 'warehouse_ids'),
OperationDao.get_return_by_request_idempotency_key: ( OperationDao.get_return_by_request_idempotency_key: (
'db', 'db',
'idempotency_key', 'idempotency_key',
'source_site_id', 'source_site_id',
'target_warehouse_id', 'target_warehouse_id',
'scope',
), ),
OperationDao.get_return_by_confirm_idempotency_key: ( OperationDao.get_return_by_confirm_idempotency_key: (
'db', 'db',
'idempotency_key', 'idempotency_key',
'source_site_id', 'source_site_id',
'target_warehouse_id', 'target_warehouse_id',
'scope',
), ),
OperationDao.get_return_by_id: ('db', 'return_id', 'site_ids', 'warehouse_ids'), OperationDao.get_return_by_id: ('db', 'return_id', 'scope', 'site_ids', 'warehouse_ids'),
} }
for method, parameters in expected.items(): for method, parameters in expected.items():
with self.subTest(method=method.__name__): with self.subTest(method=method.__name__):
@@ -208,7 +211,7 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
list_signature = inspect.signature(OperationDao.get_return_list) list_signature = inspect.signature(OperationDao.get_return_list)
self.assertEqual( self.assertEqual(
('db', 'query_object', 'is_page', 'site_ids', 'warehouse_ids'), ('db', 'query_object', 'site_ids', 'warehouse_ids', 'scope', 'is_page'),
tuple(list_signature.parameters), tuple(list_signature.parameters),
) )
self.assertIs(True, list_signature.parameters['is_page'].default) self.assertIs(True, list_signature.parameters['is_page'].default)
@@ -235,6 +238,7 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
13, 13,
site_ids=[9, 7, 9], site_ids=[9, 7, 9],
warehouse_ids=[], warehouse_ids=[],
scope=TestBatchScope.formal(),
) )
self.assertIs(order, actual) self.assertIs(order, actual)
@@ -271,6 +275,7 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
13, 13,
site_ids=[9], site_ids=[9],
warehouse_ids=[7], warehouse_ids=[7],
scope=TestBatchScope.formal(),
) )
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -302,6 +307,7 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
key, key,
source_site_id=source_site_id, source_site_id=source_site_id,
target_warehouse_id=target_warehouse_id, target_warehouse_id=target_warehouse_id,
scope=TestBatchScope.formal(),
) )
statement = self.db.execute.await_args.args[0] statement = self.db.execute.await_args.args[0]
@@ -317,6 +323,18 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
@unittest.skipUnless(ReturnService is not None, 'ReturnService 尚未实现') @unittest.skipUnless(ReturnService is not None, 'ReturnService 尚未实现')
class ReturnServiceContractRedTest(unittest.TestCase): class ReturnServiceContractRedTest(unittest.TestCase):
def test_public_business_methods_require_explicit_test_scope(self) -> None:
for method_name in (
'get_return_list_services',
'return_detail_services',
'get_warehouse_options_services',
'scan_return_pallet_services',
'request_return_services',
'confirm_return_services',
):
with self.subTest(method=method_name):
self.assertIn('test_scope', inspect.signature(getattr(ReturnService, method_name)).parameters)
def test_service_exposes_complete_return_workflow(self) -> None: def test_service_exposes_complete_return_workflow(self) -> None:
expected = { expected = {
'get_return_list_services', 'get_return_list_services',
@@ -331,9 +349,9 @@ class ReturnServiceContractRedTest(unittest.TestCase):
def test_request_and_confirm_signatures_are_exact(self) -> None: def test_request_and_confirm_signatures_are_exact(self) -> None:
request_signature = inspect.signature(ReturnService.request_return_services) request_signature = inspect.signature(ReturnService.request_return_services)
confirm_signature = inspect.signature(ReturnService.confirm_return_services) confirm_signature = inspect.signature(ReturnService.confirm_return_services)
self.assertEqual(('query_db', 'page_object', 'current_user'), tuple(request_signature.parameters)) self.assertEqual(('query_db', 'page_object', 'current_user', 'test_scope'), tuple(request_signature.parameters))
self.assertEqual( self.assertEqual(
('query_db', 'return_id', 'page_object', 'current_user'), ('query_db', 'return_id', 'page_object', 'current_user', 'test_scope'),
tuple(confirm_signature.parameters), tuple(confirm_signature.parameters),
) )
request_hints = get_type_hints(ReturnService.request_return_services) request_hints = get_type_hints(ReturnService.request_return_services)
@@ -493,15 +511,16 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 7])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 7])),
patch.object(OperationDao, 'get_return_list', new=AsyncMock(return_value=page)) as get_list, patch.object(OperationDao, 'get_return_list', new=AsyncMock(return_value=page)) as get_list,
): ):
actual = await ReturnService.get_return_list_services(self.db, query, self.user) actual = await ReturnService.get_return_list_services(self.db, query, self.user, TestBatchScope.formal())
self.assertEqual([], actual.rows) self.assertEqual([], actual.rows)
get_list.assert_awaited_once_with( get_list.assert_awaited_once_with(
self.db, self.db,
query, query,
True,
site_ids=[9], site_ids=[9],
warehouse_ids=[7], warehouse_ids=[7],
scope=TestBatchScope.formal(),
is_page=True,
) )
with ( with (
@@ -509,7 +528,7 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[])),
patch.object(OperationDao, 'get_return_list', new=AsyncMock()) as empty_get_list, patch.object(OperationDao, 'get_return_list', new=AsyncMock()) as empty_get_list,
): ):
empty = await ReturnService.get_return_list_services(self.db, query, self.user) empty = await ReturnService.get_return_list_services(self.db, query, self.user, TestBatchScope.formal())
self.assertEqual([], empty.rows) self.assertEqual([], empty.rows)
empty_get_list.assert_not_awaited() empty_get_list.assert_not_awaited()
@@ -523,7 +542,7 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)), patch.object(MasterDao, 'list_warehouse', new=AsyncMock(return_value=warehouses)),
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock()) as mappings, patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock()) as mappings,
): ):
actual = await ReturnService.get_warehouse_options_services(self.db, self.user) actual = await ReturnService.get_warehouse_options_services(self.db, self.user, TestBatchScope.formal())
self.assertEqual([3, 7], [option.id for option in actual]) self.assertEqual([3, 7], [option.id for option in actual])
mappings.assert_not_awaited() mappings.assert_not_awaited()
@@ -543,10 +562,11 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
' qr-2 ', ' qr-2 ',
ReturnScanQueryModel(mode='request'), ReturnScanQueryModel(mode='request'),
self.user, self.user,
TestBatchScope.formal(),
) )
get_pallet.assert_awaited_once_with(self.db, 'qr-2') get_pallet.assert_awaited_once_with(self.db, 'qr-2', TestBatchScope.formal())
get_inventory.assert_awaited_once_with(self.db, 2) get_inventory.assert_awaited_once_with(self.db, 2, TestBatchScope.formal())
site_access.assert_awaited_once_with(self.db, self.user, 9) site_access.assert_awaited_once_with(self.db, self.user, 9)
self.assertEqual(PalletFlowState.AT_SITE_PARTIAL, actual.flow_state) self.assertEqual(PalletFlowState.AT_SITE_PARTIAL, actual.flow_state)
self.assertEqual(8, actual.remaining_packages) self.assertEqual(8, actual.remaining_packages)
@@ -569,9 +589,10 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
'qr-2', 'qr-2',
ReturnScanQueryModel(mode='confirm', returnId=13), ReturnScanQueryModel(mode='confirm', returnId=13),
self.user, self.user,
TestBatchScope.formal(),
) )
get_order.assert_awaited_once_with(self.db, 13, site_ids=[], warehouse_ids=[7]) get_order.assert_awaited_once_with(self.db, 13, TestBatchScope.formal(), site_ids=[], warehouse_ids=[7])
self.assertEqual(PalletFlowState.RETURNING, actual.flow_state) self.assertEqual(PalletFlowState.RETURNING, actual.flow_state)
self.assertEqual(['confirm'], actual.allowed_actions) self.assertEqual(['confirm'], actual.allowed_actions)
@@ -586,7 +607,7 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=changed)), patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=changed)),
self.assertRaises(ServiceException) as changed_error, self.assertRaises(ServiceException) as changed_error,
): ):
await ReturnService.scan_return_pallet_services(self.db, 'qr-2', query, self.user) await ReturnService.scan_return_pallet_services(self.db, 'qr-2', query, self.user, TestBatchScope.formal())
self.assertIn('快照', changed_error.exception.message) self.assertIn('快照', changed_error.exception.message)
with ( with (
@@ -594,7 +615,7 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_return_by_id', new=AsyncMock(return_value=None)), patch.object(OperationDao, 'get_return_by_id', new=AsyncMock(return_value=None)),
self.assertRaises(PermissionException) as hidden_error, self.assertRaises(PermissionException) as hidden_error,
): ):
await ReturnService.scan_return_pallet_services(self.db, 'qr-2', query, self.user) await ReturnService.scan_return_pallet_services(self.db, 'qr-2', query, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该退回单', hidden_error.exception.message) self.assertEqual('无权访问该退回单', hidden_error.exception.message)
async def test_confirm_scan_hides_missing_and_foreign_pallet_equally_for_normal_user(self) -> None: async def test_confirm_scan_hides_missing_and_foreign_pallet_equally_for_normal_user(self) -> None:
@@ -611,7 +632,9 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_pallet_inventory', new=inventory_read), patch.object(MasterDao, 'get_pallet_inventory', new=inventory_read),
): ):
try: try:
await ReturnService.scan_return_pallet_services(self.db, 'qr-5', query, self.user) await ReturnService.scan_return_pallet_services(
self.db, 'qr-5', query, self.user, TestBatchScope.formal()
)
except Exception as exc: except Exception as exc:
raised = exc raised = exc
else: else:
@@ -638,7 +661,9 @@ class ReturnReadAndScanRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock()) as inventory_read, patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock()) as inventory_read,
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ReturnService.scan_return_pallet_services(self.db, 'qr-5', query, admin) await ReturnService.scan_return_pallet_services(
self.db, 'qr-5', query, admin, TestBatchScope.formal()
)
self.assertEqual(expected_message, raised.exception.message) self.assertEqual(expected_message, raised.exception.message)
inventory_read.assert_not_awaited() inventory_read.assert_not_awaited()
@@ -679,11 +704,11 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
) as site_access, ) as site_access,
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_access), patch.object(AccessService, 'assert_warehouse_access', new=warehouse_access),
): ):
await ReturnService._lock_new_request_route(self.db, self.page, self.user) await ReturnService._lock_new_request_route(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(['warehouse-row', 'site-row', 'site-mapping'], actions) self.assertEqual(['warehouse-row', 'site-row', 'site-mapping'], actions)
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_read.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
site_read.assert_awaited_once_with(self.db, 9, for_update=True) site_read.assert_awaited_once_with(self.db, 9, TestBatchScope.formal(), for_update=True)
site_access.assert_awaited_once_with(self.db, self.user, 9, for_update=True) site_access.assert_awaited_once_with(self.db, self.user, 9, for_update=True)
warehouse_access.assert_not_awaited() warehouse_access.assert_not_awaited()
@@ -752,7 +777,9 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=add_event)), patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=add_event)),
): ):
try: try:
result = await ReturnService.request_return_services(self.db, self.page, self.user) result = await ReturnService.request_return_services(
self.db, self.page, self.user, TestBatchScope.formal()
)
except RuntimeError as exc: except RuntimeError as exc:
self.fail(str(exc)) self.fail(str(exc))
@@ -839,7 +866,9 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=AsyncMock()) as add_event, patch.object(InventoryDao, 'add_event', new=AsyncMock()) as add_event,
): ):
try: try:
result = await ReturnService.request_return_services(self.db, self.page, self.user) result = await ReturnService.request_return_services(
self.db, self.page, self.user, TestBatchScope.formal()
)
except ServiceException as exc: except ServiceException as exc:
self.fail(f'锁等待后的精确重试被库存状态提前拒绝:{exc.message}') self.fail(f'锁等待后的精确重试被库存状态提前拒绝:{exc.message}')
@@ -896,7 +925,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order, patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order,
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ReturnService.request_return_services(self.db, conflict_page, self.user) await ReturnService.request_return_services(self.db, conflict_page, self.user, TestBatchScope.formal())
self.assertEqual('申请退回失败,幂等键已被其他请求使用', raised.exception.message) self.assertEqual('申请退回失败,幂等键已被其他请求使用', raised.exception.message)
self.assertEqual(2, key_read.await_count) self.assertEqual(2, key_read.await_count)
@@ -942,7 +971,9 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=AsyncMock()) as add_event, patch.object(InventoryDao, 'add_event', new=AsyncMock()) as add_event,
): ):
try: try:
result = await ReturnService.request_return_services(self.db, self.page, self.user) result = await ReturnService.request_return_services(
self.db, self.page, self.user, TestBatchScope.formal()
)
except PermissionException as exc: except PermissionException as exc:
self.fail(f'已确认赢家在 fresh key 查询前被库存位置提前隐藏:{exc.message}') self.fail(f'已确认赢家在 fresh key 查询前被库存位置提前隐藏:{exc.message}')
@@ -992,7 +1023,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order, patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order,
): ):
try: try:
await ReturnService.request_return_services(self.db, conflict_page, self.user) await ReturnService.request_return_services(self.db, conflict_page, self.user, TestBatchScope.formal())
except Exception as exc: except Exception as exc:
raised = exc raised = exc
else: else:
@@ -1032,13 +1063,21 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
), ),
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))), patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
): ):
result = await ReturnService.request_return_services(self.db, self.page, self.user) result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
self.assertEqual( self.assertEqual(
[ [
{'source_site_id': 9, 'target_warehouse_id': 7}, {
{'source_site_id': 9, 'target_warehouse_id': 7}, 'source_site_id': 9,
'target_warehouse_id': 7,
'scope': TestBatchScope.formal(),
},
{
'source_site_id': 9,
'target_warehouse_id': 7,
'scope': TestBatchScope.formal(),
},
], ],
[call.kwargs for call in key_read.await_args_list], [call.kwargs for call in key_read.await_args_list],
) )
@@ -1062,10 +1101,10 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_locks), patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_locks),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ReturnService.request_return_services(self.db, self.page, self.user) await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
inventory_locks.assert_awaited_once_with(self.db, [2, 5]) inventory_locks.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
pallet_locks.assert_not_awaited() pallet_locks.assert_not_awaited()
self.assertEqual(2, scope_read.await_count) self.assertEqual(2, scope_read.await_count)
self.assertEqual(1, key_read.await_count) self.assertEqual(1, key_read.await_count)
@@ -1112,12 +1151,12 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))), patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as inventory_locks, patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as inventory_locks,
): ):
result = await ReturnService.request_return_services(self.db, self.page, self.user) result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
self.assertTrue(probe.expired) self.assertTrue(probe.expired)
self.assertEqual(1, probe.return_id_reads) self.assertEqual(1, probe.return_id_reads)
order_lock.assert_awaited_once_with(self.db, 13, site_ids=[9], warehouse_ids=[]) order_lock.assert_awaited_once_with(self.db, 13, TestBatchScope.formal(), site_ids=[9], warehouse_ids=[])
inventory_locks.assert_not_awaited() inventory_locks.assert_not_awaited()
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -1149,7 +1188,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_locks), patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_locks),
): ):
try: try:
await ReturnService.request_return_services(db, self.page, self.user) await ReturnService.request_return_services(db, self.page, self.user, TestBatchScope.formal())
except Exception as exc: except Exception as exc:
raised = exc raised = exc
else: else:
@@ -1192,7 +1231,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))), patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
): ):
try: try:
await ReturnService.request_return_services(self.db, self.page, self.user) await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
except Exception as exc: except Exception as exc:
raised = exc raised = exc
else: else:
@@ -1285,7 +1324,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(return_service_module, 'datetime') as service_datetime, patch.object(return_service_module, 'datetime') as service_datetime,
): ):
service_datetime.now.return_value = self.fixed_time service_datetime.now.return_value = self.fixed_time
result = await ReturnService.request_return_services(self.db, self.page, self.user) result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
@@ -1293,12 +1332,20 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(2, key_read.await_count) self.assertEqual(2, key_read.await_count)
self.assertEqual( self.assertEqual(
[ [
{'source_site_id': 9, 'target_warehouse_id': 7}, {
{'source_site_id': 9, 'target_warehouse_id': 7}, 'source_site_id': 9,
'target_warehouse_id': 7,
'scope': TestBatchScope.formal(),
},
{
'source_site_id': 9,
'target_warehouse_id': 7,
'scope': TestBatchScope.formal(),
},
], ],
[call.kwargs for call in key_read.await_args_list], [call.kwargs for call in key_read.await_args_list],
) )
locks.assert_awaited_once_with(self.db, [2, 5]) locks.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
site_access.assert_awaited_with(self.db, self.user, 9, for_update=True) site_access.assert_awaited_with(self.db, self.user, 9, for_update=True)
warehouse_access.assert_not_awaited() warehouse_access.assert_not_awaited()
self.assertEqual(ReturnStatus.REQUESTED.value, captured['order'].status) self.assertEqual(ReturnStatus.REQUESTED.value, captured['order'].status)
@@ -1339,7 +1386,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order, patch.object(OperationDao, 'add_return_order', new=AsyncMock()) as add_order,
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ReturnService.request_return_services(self.db, self.page, self.user) await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
add_order.assert_not_awaited() add_order.assert_not_awaited()
@@ -1370,7 +1417,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock()) as pallet_locks, patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock()) as pallet_locks,
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ReturnService.request_return_services(db, self.page, admin) await ReturnService.request_return_services(db, self.page, admin, TestBatchScope.formal())
self.assertEqual(expected_message, raised.exception.message) self.assertEqual(expected_message, raised.exception.message)
pallet_locks.assert_not_awaited() pallet_locks.assert_not_awaited()
db.commit.assert_not_awaited() db.commit.assert_not_awaited()
@@ -1391,7 +1438,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))), patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks, patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks,
): ):
result = await ReturnService.request_return_services(self.db, self.page, self.user) result = await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
locks.assert_not_awaited() locks.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -1408,7 +1455,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_site_access', new=AsyncMock()), patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ReturnService.request_return_services(conflict_db, self.page, self.user) await ReturnService.request_return_services(conflict_db, self.page, self.user, TestBatchScope.formal())
self.assertIn('幂等键已被其他请求使用', raised.exception.message) self.assertIn('幂等键已被其他请求使用', raised.exception.message)
conflict_db.commit.assert_not_awaited() conflict_db.commit.assert_not_awaited()
conflict_db.rollback.assert_awaited() conflict_db.rollback.assert_awaited()
@@ -1435,7 +1482,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[])), patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[])),
): ):
try: try:
await ReturnService.request_return_services(self.db, self.page, self.user) await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
except Exception as exc: except Exception as exc:
raised = exc raised = exc
else: else:
@@ -1458,7 +1505,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_locks), patch.object(InventoryDao, 'get_inventories_for_update', new=inventory_locks),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ReturnService.request_return_services(self.db, self.page, self.user) await ReturnService.request_return_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
key_read.assert_not_awaited() key_read.assert_not_awaited()
@@ -1483,6 +1530,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
self.page, self.page,
request_hash, request_hash,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
@@ -1491,10 +1539,12 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
'request-key', 'request-key',
source_site_id=9, source_site_id=9,
target_warehouse_id=7, target_warehouse_id=7,
scope=TestBatchScope.formal(),
) )
order_lock.assert_awaited_once_with( order_lock.assert_awaited_once_with(
self.db, self.db,
13, 13,
TestBatchScope.formal(),
site_ids=[9, 10], site_ids=[9, 10],
warehouse_ids=[], warehouse_ids=[],
) )
@@ -1511,7 +1561,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_site_access', new=AsyncMock()), patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))), patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
): ):
result = await ReturnService.request_return_services(self.db, self.page, admin) result = await ReturnService.request_return_services(self.db, self.page, admin, TestBatchScope.formal())
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
get_by_key.assert_awaited_once_with( get_by_key.assert_awaited_once_with(
@@ -1519,6 +1569,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase):
'request-key', 'request-key',
source_site_id=9, source_site_id=9,
target_warehouse_id=7, target_warehouse_id=7,
scope=TestBatchScope.formal(),
) )
@@ -1583,12 +1634,14 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(return_service_module, 'datetime') as service_datetime, patch.object(return_service_module, 'datetime') as service_datetime,
): ):
service_datetime.now.return_value = self.fixed_time service_datetime.now.return_value = self.fixed_time
result = await ReturnService.confirm_return_services(self.db, 13, self.page, self.user) result = await ReturnService.confirm_return_services(
self.db, 13, self.page, self.user, TestBatchScope.formal()
)
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
lock_order.assert_awaited_once_with(self.db, 13, site_ids=[], warehouse_ids=[7]) lock_order.assert_awaited_once_with(self.db, 13, TestBatchScope.formal(), site_ids=[], warehouse_ids=[7])
locks.assert_awaited_once_with(self.db, [2, 5]) locks.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
warehouse_access.assert_awaited_with(self.db, self.user, 7, for_update=True) warehouse_access.assert_awaited_with(self.db, self.user, 7, for_update=True)
self.assertEqual(PalletFlowState.FILLED_IN_WAREHOUSE.value, inventories[0].flow_state) self.assertEqual(PalletFlowState.FILLED_IN_WAREHOUSE.value, inventories[0].flow_state)
self.assertEqual(102, inventories[0].active_filling_id) self.assertEqual(102, inventories[0].active_filling_id)
@@ -1633,7 +1686,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[])), patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[])),
): ):
try: try:
await ReturnService.confirm_return_services(self.db, 13, self.page, self.user) await ReturnService.confirm_return_services(self.db, 13, self.page, self.user, TestBatchScope.formal())
except ServiceException: except ServiceException:
pass pass
@@ -1643,6 +1696,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
'confirm-key', 'confirm-key',
source_site_id=9, source_site_id=9,
target_warehouse_id=7, target_warehouse_id=7,
scope=TestBatchScope.formal(),
) )
async def test_confirm_requires_exact_scan_set_before_inventory_locks(self) -> None: async def test_confirm_requires_exact_scan_set_before_inventory_locks(self) -> None:
@@ -1664,7 +1718,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks, patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks,
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ReturnService.confirm_return_services(db, 13, page, self.user) await ReturnService.confirm_return_services(db, 13, page, self.user, TestBatchScope.formal())
self.assertIn('扫码', raised.exception.message) self.assertIn('扫码', raised.exception.message)
locks.assert_not_awaited() locks.assert_not_awaited()
db.rollback.assert_awaited() db.rollback.assert_awaited()
@@ -1699,7 +1753,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'update_return_order', new=AsyncMock()) as update_order, patch.object(OperationDao, 'update_return_order', new=AsyncMock()) as update_order,
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ReturnService.confirm_return_services(db, 13, page, self.user) await ReturnService.confirm_return_services(db, 13, page, self.user, TestBatchScope.formal())
self.assertTrue('快照' in raised.exception.message or '状态' in raised.exception.message) self.assertTrue('快照' in raised.exception.message or '状态' in raised.exception.message)
update_order.assert_not_awaited() update_order.assert_not_awaited()
db.commit.assert_not_awaited() db.commit.assert_not_awaited()
@@ -1725,7 +1779,9 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))), patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks, patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock()) as locks,
): ):
result = await ReturnService.confirm_return_services(self.db, 13, self.page, self.user) result = await ReturnService.confirm_return_services(
self.db, 13, self.page, self.user, TestBatchScope.formal()
)
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
locks.assert_not_awaited() locks.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -1744,7 +1800,9 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
), ),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ReturnService.confirm_return_services(conflict_db, 13, conflict_page, self.user) await ReturnService.confirm_return_services(
conflict_db, 13, conflict_page, self.user, TestBatchScope.formal()
)
self.assertIn('幂等键已被其他请求使用', raised.exception.message) self.assertIn('幂等键已被其他请求使用', raised.exception.message)
conflict_db.rollback.assert_awaited() conflict_db.rollback.assert_awaited()
@@ -1769,12 +1827,13 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()), patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))), patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))),
): ):
result = await ReturnService.confirm_return_services(self.db, 13, self.page, admin) result = await ReturnService.confirm_return_services(self.db, 13, self.page, admin, TestBatchScope.formal())
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
get_order.assert_awaited_once_with( get_order.assert_awaited_once_with(
self.db, self.db,
13, 13,
TestBatchScope.formal(),
site_ids=None, site_ids=None,
warehouse_ids=None, warehouse_ids=None,
) )
@@ -1783,6 +1842,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
'confirm-key', 'confirm-key',
source_site_id=9, source_site_id=9,
target_warehouse_id=7, target_warehouse_id=7,
scope=TestBatchScope.formal(),
) )
async def test_confirm_integrity_recovery_resolves_route_before_exact_key_lookup(self) -> None: async def test_confirm_integrity_recovery_resolves_route_before_exact_key_lookup(self) -> None:
@@ -1819,18 +1879,20 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
self.page, self.page,
confirm_hash, confirm_hash,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertEqual(13, result.result) self.assertEqual(13, result.result)
self.assertEqual(['return', 'key'], call_order) self.assertEqual(['return', 'key'], call_order)
route_read.assert_awaited_once_with(self.db, 13, site_ids=[], warehouse_ids=[7, 8]) route_read.assert_awaited_once_with(self.db, 13, TestBatchScope.formal(), site_ids=[], warehouse_ids=[7, 8])
key_read.assert_awaited_once_with( key_read.assert_awaited_once_with(
self.db, self.db,
'confirm-key', 'confirm-key',
source_site_id=9, source_site_id=9,
target_warehouse_id=7, target_warehouse_id=7,
scope=TestBatchScope.formal(),
) )
order_lock.assert_awaited_once_with(self.db, 13, site_ids=[], warehouse_ids=[7, 8]) order_lock.assert_awaited_once_with(self.db, 13, TestBatchScope.formal(), site_ids=[], warehouse_ids=[7, 8])
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
async def test_confirm_rechecks_warehouse_mapping_before_business_locks(self) -> None: async def test_confirm_rechecks_warehouse_mapping_before_business_locks(self) -> None:
@@ -1857,7 +1919,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[])), patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[])),
): ):
try: try:
await ReturnService.confirm_return_services(self.db, 13, self.page, self.user) await ReturnService.confirm_return_services(self.db, 13, self.page, self.user, TestBatchScope.formal())
except Exception as exc: except Exception as exc:
raised = exc raised = exc
else: else:
@@ -23,7 +23,7 @@ from common.vo import (
PageResponseModel, PageResponseModel,
ResponseBaseModel, ResponseBaseModel,
) )
from exceptions.exception import PermissionException, ServiceException from exceptions.exception import PermissionException, ServiceException, ServiceWarning
from module_admin.entity.vo.user_vo import CurrentUserModel, UserInfoModel from module_admin.entity.vo.user_vo import CurrentUserModel, UserInfoModel
from module_admin.service.login_service import LoginService from module_admin.service.login_service import LoginService
from module_business.controller import shipment_controller as shipment_controller_module from module_business.controller import shipment_controller as shipment_controller_module
@@ -33,6 +33,7 @@ from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.pallet_state_machine import PalletStateMachine from module_business.domain.pallet_state_machine import PalletStateMachine
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizWarehouse from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizWarehouse
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
@@ -152,6 +153,21 @@ def make_shipment(
class ShipmentServiceBootstrapTest(unittest.TestCase): class ShipmentServiceBootstrapTest(unittest.TestCase):
def test_public_business_methods_require_explicit_test_scope(self) -> None:
for method_name in (
'get_shipment_list_services',
'shipment_detail_services',
'get_warehouse_options_services',
'get_site_options_services',
'scan_shipment_pallet_services',
'add_shipment_services',
'edit_shipment_services',
'delete_shipment_services',
'confirm_shipment_services',
):
with self.subTest(method=method_name):
self.assertIn('test_scope', inspect.signature(getattr(ShipmentService, method_name)).parameters)
def test_shipment_service_module_exists(self) -> None: def test_shipment_service_module_exists(self) -> None:
spec = importlib.util.find_spec('module_business.service.shipment_service') spec = importlib.util.find_spec('module_business.service.shipment_service')
@@ -164,7 +180,7 @@ class ShipmentServiceBootstrapTest(unittest.TestCase):
) )
signature = inspect.signature(ShipmentService.confirm_shipment_services) signature = inspect.signature(ShipmentService.confirm_shipment_services)
self.assertEqual( self.assertEqual(
('query_db', 'shipment_id', 'page_object', 'current_user'), ('query_db', 'shipment_id', 'page_object', 'current_user', 'test_scope'),
tuple(signature.parameters), tuple(signature.parameters),
) )
hints = get_type_hints(ShipmentService.confirm_shipment_services) hints = get_type_hints(ShipmentService.confirm_shipment_services)
@@ -215,25 +231,29 @@ class ShipmentInfrastructureContractTest(unittest.TestCase):
OperationDao.get_shipment_list: ( OperationDao.get_shipment_list: (
'db', 'db',
'query_object', 'query_object',
'is_page',
'warehouse_ids', 'warehouse_ids',
'site_ids', 'site_ids',
'scope',
'is_page',
), ),
OperationDao.get_shipment_by_id: ( OperationDao.get_shipment_by_id: (
'db', 'db',
'shipment_id', 'shipment_id',
'scope',
'warehouse_ids', 'warehouse_ids',
'site_ids', 'site_ids',
), ),
OperationDao.get_shipment_for_update: ( OperationDao.get_shipment_for_update: (
'db', 'db',
'shipment_id', 'shipment_id',
'scope',
'warehouse_ids', 'warehouse_ids',
'site_ids', 'site_ids',
), ),
OperationDao.get_shipment_by_confirm_idempotency_key: ( OperationDao.get_shipment_by_confirm_idempotency_key: (
'db', 'db',
'idempotency_key', 'idempotency_key',
'scope',
'warehouse_ids', 'warehouse_ids',
'site_ids', 'site_ids',
), ),
@@ -271,6 +291,7 @@ class ShipmentDetailDaoTest(unittest.IsolatedAsyncioTestCase):
12, 12,
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
scope=TestBatchScope.formal(),
) )
self.assertIs(shipment, actual) self.assertIs(shipment, actual)
@@ -322,6 +343,7 @@ class ShipmentDetailDaoTest(unittest.IsolatedAsyncioTestCase):
12, 12,
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
scope=TestBatchScope.formal(),
) )
self.assertIs(shipment, actual) self.assertIs(shipment, actual)
@@ -472,6 +494,7 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
query, query,
self.user, self.user,
TestBatchScope.formal(),
is_page=True, is_page=True,
) )
except NotImplementedError: except NotImplementedError:
@@ -500,9 +523,10 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
ShipmentPageQueryModel(), ShipmentPageQueryModel(),
self.user, self.user,
TestBatchScope.formal(),
) )
try: try:
detail = await ShipmentService.shipment_detail_services(self.db, 12, self.user) detail = await ShipmentService.shipment_detail_services(self.db, 12, self.user, TestBatchScope.formal())
except NotImplementedError: except NotImplementedError:
self.fail('双范围单据详情尚未实现') self.fail('双范围单据详情尚未实现')
@@ -511,13 +535,15 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
get_list.assert_awaited_once_with( get_list.assert_awaited_once_with(
self.db, self.db,
unittest.mock.ANY, unittest.mock.ANY,
True,
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
scope=TestBatchScope.formal(),
is_page=True,
) )
get_detail.assert_awaited_once_with( get_detail.assert_awaited_once_with(
self.db, self.db,
12, 12,
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
) )
@@ -540,8 +566,12 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'list_site', new=AsyncMock(return_value=sites)), patch.object(MasterDao, 'list_site', new=AsyncMock(return_value=sites)),
): ):
try: try:
warehouse_options = await ShipmentService.get_warehouse_options_services(self.db, self.user) warehouse_options = await ShipmentService.get_warehouse_options_services(
site_options = await ShipmentService.get_site_options_services(self.db, self.user) self.db, self.user, TestBatchScope.formal()
)
site_options = await ShipmentService.get_site_options_services(
self.db, self.user, TestBatchScope.formal()
)
except NotImplementedError: except NotImplementedError:
self.fail('发货仓库/现场选项尚未实现') self.fail('发货仓库/现场选项尚未实现')
@@ -598,6 +628,7 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
'qr-2', 'qr-2',
query, query,
self.user, self.user,
TestBatchScope.formal(),
) )
except NotImplementedError: except NotImplementedError:
self.fail('整托发货扫码尚未实现') self.fail('整托发货扫码尚未实现')
@@ -606,7 +637,11 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(PalletFlowState.FILLED_IN_WAREHOUSE, result.flow_state) self.assertEqual(PalletFlowState.FILLED_IN_WAREHOUSE, result.flow_state)
self.assertEqual(8, result.remaining_packages) self.assertEqual(8, result.remaining_packages)
self.assertEqual(['shipment'], result.allowed_actions) self.assertEqual(['shipment'], result.allowed_actions)
get_filling.assert_awaited_once_with(self.db, inventory.active_filling_id) get_filling.assert_awaited_once_with(
self.db,
inventory.active_filling_id,
TestBatchScope.formal(),
)
lock_inventory.assert_not_awaited() lock_inventory.assert_not_awaited()
lock_pallets.assert_not_awaited() lock_pallets.assert_not_awaited()
lock_filling.assert_not_awaited() lock_filling.assert_not_awaited()
@@ -627,7 +662,7 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_shipment_list', new=get_list), patch.object(OperationDao, 'get_shipment_list', new=get_list),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ShipmentService.get_shipment_list_services(self.db, query, self.user) await ShipmentService.get_shipment_list_services(self.db, query, self.user, TestBatchScope.formal())
self.assertEqual(expected_message, raised.exception.message) self.assertEqual(expected_message, raised.exception.message)
get_list.assert_not_awaited() get_list.assert_not_awaited()
@@ -640,12 +675,13 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_shipment_by_id', new=get_detail), patch.object(OperationDao, 'get_shipment_by_id', new=get_detail),
self.assertRaises(PermissionException) as user_raised, self.assertRaises(PermissionException) as user_raised,
): ):
await ShipmentService.shipment_detail_services(self.db, 404, self.user) await ShipmentService.shipment_detail_services(self.db, 404, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该发货单', user_raised.exception.message) self.assertEqual('无权访问该发货单', user_raised.exception.message)
get_detail.assert_awaited_once_with( get_detail.assert_awaited_once_with(
self.db, self.db,
404, 404,
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
) )
@@ -655,11 +691,12 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_shipment_by_id', new=get_detail), patch.object(OperationDao, 'get_shipment_by_id', new=get_detail),
self.assertRaises(ServiceException) as admin_raised, self.assertRaises(ServiceException) as admin_raised,
): ):
await ShipmentService.shipment_detail_services(self.db, 404, make_user(1)) await ShipmentService.shipment_detail_services(self.db, 404, make_user(1), TestBatchScope.formal())
self.assertEqual('发货单不存在', admin_raised.exception.message) self.assertEqual('发货单不存在', admin_raised.exception.message)
get_detail.assert_awaited_once_with( get_detail.assert_awaited_once_with(
self.db, self.db,
404, 404,
TestBatchScope.formal(),
warehouse_ids=None, warehouse_ids=None,
site_ids=None, site_ids=None,
) )
@@ -715,6 +752,7 @@ class ShipmentReadServiceTest(unittest.IsolatedAsyncioTestCase):
'hidden-token', 'hidden-token',
ShipmentScanQueryModel(sourceWarehouseId=7, targetSiteId=9), ShipmentScanQueryModel(sourceWarehouseId=7, targetSiteId=9),
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
@@ -747,6 +785,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
[2], [2],
7, 7,
TestBatchScope.formal(),
) )
self.assertEqual([inventory], inventories) self.assertEqual([inventory], inventories)
@@ -794,7 +833,11 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
actions.append('pallet-lock') actions.append('pallet-lock')
return pallets return pallets
async def lock_filling(_db: AsyncSession, filling_id: int) -> BizFillingRecord: async def lock_filling(
_db: AsyncSession,
filling_id: int,
_scope: TestBatchScope,
) -> BizFillingRecord:
actions.append(f'filling-lock-{filling_id}') actions.append(f'filling-lock-{filling_id}')
return fillings[filling_id] return fillings[filling_id]
@@ -844,7 +887,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
service_datetime.now.return_value = self.fixed_time service_datetime.now.return_value = self.fixed_time
service_secrets.token_hex.return_value = 'draft-token' service_secrets.token_hex.return_value = 'draft-token'
try: try:
result = await ShipmentService.add_shipment_services(self.db, page, self.user) result = await ShipmentService.add_shipment_services(self.db, page, self.user, TestBatchScope.formal())
except NotImplementedError: except NotImplementedError:
self.fail('发货草稿新增尚未实现') self.fail('发货草稿新增尚未实现')
@@ -854,8 +897,8 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual([2, 5], [item.pallet_id for item in captured['items']]) self.assertEqual([2, 5], [item.pallet_id for item in captured['items']])
self.assertTrue(all(item.active_filling_id is None for item in captured['items'])) self.assertTrue(all(item.active_filling_id is None for item in captured['items']))
self.assertTrue(all(item.package_qty is None for item in captured['items'])) self.assertTrue(all(item.package_qty is None for item in captured['items']))
inventory_lock.assert_awaited_once_with(self.db, [2, 5]) inventory_lock.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
pallet_lock.assert_awaited_once_with(self.db, [2, 5]) pallet_lock.assert_awaited_once_with(self.db, [2, 5], TestBatchScope.formal())
self.assertEqual( self.assertEqual(
[ [
'warehouse-access', 'warehouse-access',
@@ -928,11 +971,11 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'replace_shipment_items', new=replace_items), patch.object(OperationDao, 'replace_shipment_items', new=replace_items),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ShipmentService.add_shipment_services(self.db, page, self.user) await ShipmentService.add_shipment_services(self.db, page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该仓库', raised.exception.message) self.assertEqual('无权访问该仓库', raised.exception.message)
self.assertEqual(2, warehouse_read.await_count) self.assertEqual(2, warehouse_read.await_count)
warehouse_read.assert_awaited_with(self.db, 7, for_update=True) warehouse_read.assert_awaited_with(self.db, 7, TestBatchScope.formal(), for_update=True)
self.assertEqual(2, warehouse_mapping.await_count) self.assertEqual(2, warehouse_mapping.await_count)
warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True) warehouse_mapping.assert_awaited_with(self.db, self.user, 7, for_update=True)
add_order.assert_not_awaited() add_order.assert_not_awaited()
@@ -980,10 +1023,10 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'replace_shipment_items', new=replace_items), patch.object(OperationDao, 'replace_shipment_items', new=replace_items),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ShipmentService.add_shipment_services(self.db, page, self.user) await ShipmentService.add_shipment_services(self.db, page, self.user, TestBatchScope.formal())
self.assertEqual('来源仓库已停用', raised.exception.message) self.assertEqual('来源仓库已停用', raised.exception.message)
warehouse_read.assert_awaited_with(self.db, 7, for_update=True) warehouse_read.assert_awaited_with(self.db, 7, TestBatchScope.formal(), for_update=True)
add_order.assert_not_awaited() add_order.assert_not_awaited()
replace_items.assert_not_awaited() replace_items.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -1062,7 +1105,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
): ):
service_datetime.now.return_value = self.fixed_time service_datetime.now.return_value = self.fixed_time
try: try:
result = await ShipmentService.edit_shipment_services(self.db, page, self.user) result = await ShipmentService.edit_shipment_services(self.db, page, self.user, TestBatchScope.formal())
except NotImplementedError: except NotImplementedError:
self.fail('发货草稿编辑尚未实现') self.fail('发货草稿编辑尚未实现')
@@ -1070,6 +1113,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
lock_order.assert_awaited_once_with( lock_order.assert_awaited_once_with(
self.db, self.db,
12, 12,
TestBatchScope.formal(),
warehouse_ids=[7, 8], warehouse_ids=[7, 8],
site_ids=[9, 10], site_ids=[9, 10],
) )
@@ -1129,6 +1173,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
async def get_warehouse( async def get_warehouse(
_db: AsyncSession, _db: AsyncSession,
warehouse_id: int, warehouse_id: int,
_scope: TestBatchScope,
**_kwargs: Any, **_kwargs: Any,
) -> BizWarehouse: ) -> BizWarehouse:
return BizWarehouse( return BizWarehouse(
@@ -1141,6 +1186,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
async def get_site( async def get_site(
_db: AsyncSession, _db: AsyncSession,
site_id: int, site_id: int,
_scope: TestBatchScope,
_scenario: str = scenario, _scenario: str = scenario,
_new_site_id: int = new_site_id, _new_site_id: int = new_site_id,
**kwargs: Any, **kwargs: Any,
@@ -1179,13 +1225,13 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'replace_shipment_items', new=replace_items), patch.object(OperationDao, 'replace_shipment_items', new=replace_items),
self.assertRaises(exception_type) as raised, self.assertRaises(exception_type) as raised,
): ):
await ShipmentService.edit_shipment_services(db, page, self.user) await ShipmentService.edit_shipment_services(db, page, self.user, TestBatchScope.formal())
self.assertEqual(expected_message, raised.exception.message) self.assertEqual(expected_message, raised.exception.message)
if scenario == 'current-warehouse-revoked': if scenario == 'current-warehouse-revoked':
warehouse_mapping.assert_any_await(db, self.user, 7, for_update=True) warehouse_mapping.assert_any_await(db, self.user, 7, for_update=True)
else: else:
site_read.assert_any_await(db, 10, for_update=True) site_read.assert_any_await(db, 10, TestBatchScope.formal(), for_update=True)
if scenario == 'new-site-revoked': if scenario == 'new-site-revoked':
site_mapping.assert_any_await(db, self.user, 10, for_update=True) site_mapping.assert_any_await(db, self.user, 10, for_update=True)
update.assert_not_awaited() update.assert_not_awaited()
@@ -1263,6 +1309,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
async def get_warehouse( async def get_warehouse(
_db: AsyncSession, _db: AsyncSession,
warehouse_id: int, warehouse_id: int,
_scope: TestBatchScope,
_disabled_ids: set[int] = disabled_warehouse_ids, _disabled_ids: set[int] = disabled_warehouse_ids,
_locks: list[str] = locks, _locks: list[str] = locks,
**kwargs: Any, **kwargs: Any,
@@ -1279,6 +1326,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
async def get_site( async def get_site(
_db: AsyncSession, _db: AsyncSession,
site_id: int, site_id: int,
_scope: TestBatchScope,
_disabled_ids: set[int] = disabled_site_ids, _disabled_ids: set[int] = disabled_site_ids,
_locks: list[str] = locks, _locks: list[str] = locks,
**kwargs: Any, **kwargs: Any,
@@ -1316,7 +1364,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'replace_shipment_items', new=replace_items), patch.object(OperationDao, 'replace_shipment_items', new=replace_items),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ShipmentService.edit_shipment_services(db, page, self.user) await ShipmentService.edit_shipment_services(db, page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该发货单', raised.exception.message) self.assertEqual('无权访问该发货单', raised.exception.message)
self.assertEqual(expected_locks, locks) self.assertEqual(expected_locks, locks)
@@ -1361,6 +1409,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
async def get_warehouse( async def get_warehouse(
_db: AsyncSession, _db: AsyncSession,
warehouse_id: int, warehouse_id: int,
_scope: TestBatchScope,
**kwargs: Any, **kwargs: Any,
) -> BizWarehouse: ) -> BizWarehouse:
if kwargs.get('for_update'): if kwargs.get('for_update'):
@@ -1375,6 +1424,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
async def get_site( async def get_site(
_db: AsyncSession, _db: AsyncSession,
site_id: int, site_id: int,
_scope: TestBatchScope,
**kwargs: Any, **kwargs: Any,
) -> BizCustomerSite: ) -> BizCustomerSite:
if kwargs.get('for_update'): if kwargs.get('for_update'):
@@ -1415,7 +1465,7 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'update_shipment', new=AsyncMock(side_effect=update)), patch.object(OperationDao, 'update_shipment', new=AsyncMock(side_effect=update)),
patch.object(OperationDao, 'replace_shipment_items', new=AsyncMock(side_effect=replace)), patch.object(OperationDao, 'replace_shipment_items', new=AsyncMock(side_effect=replace)),
): ):
result = await ShipmentService.edit_shipment_services(db, page, self.user) result = await ShipmentService.edit_shipment_services(db, page, self.user, TestBatchScope.formal())
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
self.assertEqual( self.assertEqual(
@@ -1437,222 +1487,55 @@ class ShipmentDraftServiceTest(unittest.IsolatedAsyncioTestCase):
) )
async def test_batch_delete_preflights_all_drafts_before_any_delete(self) -> None: async def test_batch_delete_preflights_all_drafts_before_any_delete(self) -> None:
documents = { lock = AsyncMock()
11: make_shipment(11, status='DRAFT'),
12: make_shipment(12, status='CONFIRMED'),
}
async def lock_order(
_db: AsyncSession,
shipment_id: int,
**_kwargs: Any,
) -> BizShipmentOrder:
return documents[shipment_id]
delete = AsyncMock() delete = AsyncMock()
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])), patch.object(OperationDao, 'get_shipment_for_update', new=lock),
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9])),
patch.object(OperationDao, 'get_shipment_for_update', new=AsyncMock(side_effect=lock_order)) as lock,
patch.object(OperationDao, 'delete_shipment', new=delete), patch.object(OperationDao, 'delete_shipment', new=delete),
self.assertRaises(ServiceWarning) as raised,
): ):
try: await ShipmentService.delete_shipment_services(self.db, [12, 11], self.user, TestBatchScope.formal())
with self.assertRaises(ServiceException):
await ShipmentService.delete_shipment_services(self.db, [12, 11], self.user)
except NotImplementedError:
self.fail('发货草稿原子批量删除尚未实现')
self.assertEqual([11, 12], [call.args[1] for call in lock.await_args_list]) self.assertEqual('不支持单条物理删除,请通过测试数据批次清理', raised.exception.message)
lock.assert_not_awaited()
delete.assert_not_awaited() delete.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_batch_delete_rechecks_all_current_routes_before_first_delete(self) -> None: async def test_batch_delete_rechecks_all_current_routes_before_first_delete(self) -> None:
documents = { lock = AsyncMock()
11: make_shipment(11, source_warehouse_id=8, target_site_id=10, status='DRAFT'),
12: make_shipment(12, source_warehouse_id=7, target_site_id=9, status='DRAFT'),
}
revoked_warehouse_id = documents[11].source_warehouse_id
parent_lock_order: list[int] = []
async def lock_order(
_db: AsyncSession,
shipment_id: int,
**_kwargs: Any,
) -> BizShipmentOrder:
parent_lock_order.append(shipment_id)
return documents[shipment_id]
async def get_warehouse(
_db: AsyncSession,
warehouse_id: int,
**_kwargs: Any,
) -> BizWarehouse:
return BizWarehouse(
warehouse_id=warehouse_id,
warehouse_code=f'W-{warehouse_id}',
warehouse_name=f'{warehouse_id}号仓',
status='0',
)
async def get_site(
_db: AsyncSession,
site_id: int,
**_kwargs: Any,
) -> BizCustomerSite:
return BizCustomerSite(
site_id=site_id,
customer_id=1,
site_code=f'S-{site_id}',
site_name=f'{site_id}号现场',
status='0',
)
async def warehouse_access(
_db: AsyncSession,
_user: CurrentUserModel,
warehouse_id: int,
**kwargs: Any,
) -> None:
if warehouse_id == revoked_warehouse_id and kwargs.get('for_update'):
raise PermissionException(data='', message='无权访问该仓库')
delete = AsyncMock() delete = AsyncMock()
warehouse_mapping = AsyncMock(side_effect=warehouse_access)
site_lock = AsyncMock(side_effect=get_site)
site_mapping = AsyncMock()
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 8])), patch.object(OperationDao, 'get_shipment_for_update', new=lock),
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9, 10])),
patch.object(OperationDao, 'get_shipment_for_update', new=AsyncMock(side_effect=lock_order)),
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(side_effect=get_warehouse)),
patch.object(AccessService, 'assert_warehouse_access', new=warehouse_mapping),
patch.object(MasterDao, 'get_site', new=site_lock),
patch.object(AccessService, 'assert_site_access', new=site_mapping),
patch.object(OperationDao, 'delete_shipment', new=delete), patch.object(OperationDao, 'delete_shipment', new=delete),
self.assertRaises(PermissionException) as raised, self.assertRaises(ServiceWarning) as raised,
): ):
await ShipmentService.delete_shipment_services(self.db, [12, 11], self.user) await ShipmentService.delete_shipment_services(self.db, [12, 11], self.user, TestBatchScope.formal())
self.assertEqual('无权访问该发货单', raised.exception.message) self.assertEqual('不支持单条物理删除,请通过测试数据批次清理', raised.exception.message)
self.assertEqual([11, 12], parent_lock_order) lock.assert_not_awaited()
self.assertEqual([7, 8], [call.args[2] for call in warehouse_mapping.await_args_list])
self.assertTrue(all(call.kwargs.get('for_update') for call in warehouse_mapping.await_args_list))
self.assertEqual([9, 10], [call.args[1] for call in site_lock.await_args_list])
self.assertEqual([9, 10], [call.args[2] for call in site_mapping.await_args_list])
self.assertTrue(all(call.kwargs.get('for_update') for call in site_mapping.await_args_list))
delete.assert_not_awaited() delete.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_batch_delete_locks_deduplicated_routes_stably_without_requiring_active(self) -> None: async def test_batch_delete_locks_deduplicated_routes_stably_without_requiring_active(self) -> None:
db = MagicMock(spec=AsyncSession) db = MagicMock(spec=AsyncSession)
documents = { lock = AsyncMock()
11: make_shipment(11, source_warehouse_id=8, target_site_id=10, status='DRAFT'), delete = AsyncMock()
12: make_shipment(12, source_warehouse_id=7, target_site_id=10, status='DRAFT'),
13: make_shipment(13, source_warehouse_id=8, target_site_id=9, status='DRAFT'),
}
actions: list[str] = []
async def lock_order(
_db: AsyncSession,
shipment_id: int,
**_kwargs: Any,
) -> BizShipmentOrder:
actions.append(f'parent-{shipment_id}')
return documents[shipment_id]
async def get_warehouse(
_db: AsyncSession,
warehouse_id: int,
**kwargs: Any,
) -> BizWarehouse:
self.assertTrue(kwargs.get('for_update'))
actions.append(f'warehouse-master-{warehouse_id}')
return BizWarehouse(
warehouse_id=warehouse_id,
warehouse_code=f'W-{warehouse_id}',
warehouse_name=f'{warehouse_id}号仓',
status='1',
)
async def warehouse_access(
_db: AsyncSession,
_user: CurrentUserModel,
warehouse_id: int,
**kwargs: Any,
) -> None:
self.assertTrue(kwargs.get('for_update'))
actions.append(f'warehouse-mapping-{warehouse_id}')
async def get_site(
_db: AsyncSession,
site_id: int,
**kwargs: Any,
) -> BizCustomerSite:
self.assertTrue(kwargs.get('for_update'))
actions.append(f'site-master-{site_id}')
return BizCustomerSite(
site_id=site_id,
customer_id=1,
site_code=f'S-{site_id}',
site_name=f'{site_id}号现场',
status='1',
)
async def site_access(
_db: AsyncSession,
_user: CurrentUserModel,
site_id: int,
**kwargs: Any,
) -> None:
self.assertTrue(kwargs.get('for_update'))
actions.append(f'site-mapping-{site_id}')
async def delete(_db: AsyncSession, shipment_id: int) -> None:
actions.append(f'delete-{shipment_id}')
async def commit() -> None:
actions.append('commit')
db.commit.side_effect = commit
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7, 8])), patch.object(OperationDao, 'get_shipment_for_update', new=lock),
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9, 10])), patch.object(OperationDao, 'delete_shipment', new=delete),
patch.object(OperationDao, 'get_shipment_for_update', new=AsyncMock(side_effect=lock_order)), self.assertRaises(ServiceWarning) as raised,
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(side_effect=get_warehouse)),
patch.object(
AccessService,
'assert_warehouse_access',
new=AsyncMock(side_effect=warehouse_access),
),
patch.object(MasterDao, 'get_site', new=AsyncMock(side_effect=get_site)),
patch.object(AccessService, 'assert_site_access', new=AsyncMock(side_effect=site_access)),
patch.object(OperationDao, 'delete_shipment', new=AsyncMock(side_effect=delete)),
): ):
result = await ShipmentService.delete_shipment_services(db, [13, 11, 12], self.user) await ShipmentService.delete_shipment_services(
db,
[13, 11, 12],
self.user,
TestBatchScope.formal(),
)
self.assertTrue(result.is_success) self.assertEqual('不支持单条物理删除,请通过测试数据批次清理', raised.exception.message)
self.assertEqual( lock.assert_not_awaited()
[ delete.assert_not_awaited()
'parent-11', db.commit.assert_not_awaited()
'parent-12',
'parent-13',
'warehouse-master-7',
'warehouse-mapping-7',
'warehouse-master-8',
'warehouse-mapping-8',
'site-master-9',
'site-mapping-9',
'site-master-10',
'site-mapping-10',
'delete-11',
'delete-12',
'delete-13',
'commit',
],
actions,
)
class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase): class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
@@ -1695,7 +1578,11 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
actions.append('pallet-lock') actions.append('pallet-lock')
return pallets return pallets
async def filling_lock(_db: AsyncSession, filling_id: int) -> BizFillingRecord: async def filling_lock(
_db: AsyncSession,
filling_id: int,
_scope: TestBatchScope,
) -> BizFillingRecord:
actions.append(f'filling-lock-{filling_id}') actions.append(f'filling-lock-{filling_id}')
return fillings[filling_id] return fillings[filling_id]
@@ -1791,6 +1678,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
except NotImplementedError: except NotImplementedError:
self.fail('多托盘整托确认尚未实现') self.fail('多托盘整托确认尚未实现')
@@ -1821,6 +1709,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
detail_read.assert_awaited_once_with( detail_read.assert_awaited_once_with(
self.db, self.db,
12, 12,
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
) )
@@ -1923,12 +1812,13 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_lock.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
warehouse_access.assert_awaited_once_with(self.db, self.user, 7, for_update=True) warehouse_access.assert_awaited_once_with(self.db, self.user, 7, for_update=True)
site_lock.assert_awaited_once_with(self.db, 9, for_update=True) site_lock.assert_awaited_once_with(self.db, 9, TestBatchScope.formal(), for_update=True)
site_access.assert_awaited_once_with(self.db, self.user, 9, for_update=True) site_access.assert_awaited_once_with(self.db, self.user, 9, for_update=True)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
update.assert_not_awaited() update.assert_not_awaited()
@@ -2009,12 +1899,13 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
warehouse_lock.assert_awaited_once_with(db, 7, for_update=True) warehouse_lock.assert_awaited_once_with(db, 7, TestBatchScope.formal(), for_update=True)
warehouse_access.assert_awaited_once_with(db, self.user, 7, for_update=True) warehouse_access.assert_awaited_once_with(db, self.user, 7, for_update=True)
site_lock.assert_awaited_once_with(db, 9, for_update=True) site_lock.assert_awaited_once_with(db, 9, TestBatchScope.formal(), for_update=True)
site_access.assert_awaited_once_with(db, self.user, 9, for_update=True) site_access.assert_awaited_once_with(db, self.user, 9, for_update=True)
self.assertEqual( self.assertEqual(
original_snapshots, original_snapshots,
@@ -2081,12 +1972,13 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertEqual('无权访问该发货单', raised.exception.message) self.assertEqual('无权访问该发货单', raised.exception.message)
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_lock.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
warehouse_mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True) warehouse_mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True)
site_lock.assert_awaited_once_with(self.db, 9, for_update=True) site_lock.assert_awaited_once_with(self.db, 9, TestBatchScope.formal(), for_update=True)
site_mapping.assert_awaited_once_with(self.db, self.user, 9, for_update=True) site_mapping.assert_awaited_once_with(self.db, self.user, 9, for_update=True)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
update.assert_not_awaited() update.assert_not_awaited()
@@ -2146,10 +2038,11 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
lock_validate.assert_awaited_once_with(self.db, [2], 7) lock_validate.assert_awaited_once_with(self.db, [2], 7, TestBatchScope.formal())
self.assertEqual((102, 6), (locked.items[0].active_filling_id, locked.items[0].package_qty)) self.assertEqual((102, 6), (locked.items[0].active_filling_id, locked.items[0].package_qty))
self.assertIsNone(initial.items[0].active_filling_id) self.assertIsNone(initial.items[0].active_filling_id)
self.assertIsNone(initial.items[0].package_qty) self.assertIsNone(initial.items[0].package_qty)
@@ -2175,7 +2068,9 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_shipment_for_update', new=parent_lock), patch.object(OperationDao, 'get_shipment_for_update', new=parent_lock),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ShipmentService.confirm_shipment_services(self.db, 12, self.command, self.user) await ShipmentService.confirm_shipment_services(
self.db, 12, self.command, self.user, TestBatchScope.formal()
)
self.assertEqual('确认发货失败,幂等键已被其他请求使用', raised.exception.message) self.assertEqual('确认发货失败,幂等键已被其他请求使用', raised.exception.message)
parent_lock.assert_not_awaited() parent_lock.assert_not_awaited()
@@ -2199,6 +2094,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
async def read_key( async def read_key(
_db: AsyncSession, _db: AsyncSession,
_key: str, _key: str,
_scope: TestBatchScope,
*, *,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None, site_ids: list[int] | None = None,
@@ -2254,18 +2150,21 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
get_detail.assert_awaited_once_with( get_detail.assert_awaited_once_with(
self.db, self.db,
12, 12,
TestBatchScope.formal(),
warehouse_ids=[7, 8], warehouse_ids=[7, 8],
site_ids=[9, 10], site_ids=[9, 10],
) )
key_read.assert_awaited_once_with( key_read.assert_awaited_once_with(
self.db, self.db,
'confirm-key', 'confirm-key',
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
) )
@@ -2295,7 +2194,9 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
), ),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ShipmentService.confirm_shipment_services(self.db, 12, self.command, self.user) await ShipmentService.confirm_shipment_services(
self.db, 12, self.command, self.user, TestBatchScope.formal()
)
self.assertEqual('确认发货失败,幂等键已被其他请求使用', raised.exception.message) self.assertEqual('确认发货失败,幂等键已被其他请求使用', raised.exception.message)
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -2359,19 +2260,22 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_site_access', new=AsyncMock()), patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ShipmentService.confirm_shipment_services(self.db, 12, self.command, self.user) await ShipmentService.confirm_shipment_services(
self.db, 12, self.command, self.user, TestBatchScope.formal()
)
self.assertEqual('无权访问该发货单', raised.exception.message) self.assertEqual('无权访问该发货单', raised.exception.message)
self.assertEqual(2, parent_lock.await_count) self.assertEqual(2, parent_lock.await_count)
parent_lock.assert_awaited_with( parent_lock.assert_awaited_with(
self.db, self.db,
12, 12,
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
) )
warehouse_lock.assert_awaited_once_with(self.db, 7, for_update=True) warehouse_lock.assert_awaited_once_with(self.db, 7, TestBatchScope.formal(), for_update=True)
warehouse_mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True) warehouse_mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True)
site_lock.assert_awaited_once_with(self.db, 9, for_update=True) site_lock.assert_awaited_once_with(self.db, 9, TestBatchScope.formal(), for_update=True)
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -2425,14 +2329,16 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await ShipmentService.confirm_shipment_services(self.db, 12, self.command, self.user) await ShipmentService.confirm_shipment_services(
self.db, 12, self.command, self.user, TestBatchScope.formal()
)
self.assertEqual('来源仓库已停用', raised.exception.message) self.assertEqual('来源仓库已停用', raised.exception.message)
self.assertEqual('DRAFT', shipment.status) self.assertEqual('DRAFT', shipment.status)
self.assertEqual('FILLED_IN_WAREHOUSE', inventory.flow_state) self.assertEqual('FILLED_IN_WAREHOUSE', inventory.flow_state)
self.assertEqual(4, inventory.version) self.assertEqual(4, inventory.version)
mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True) mapping.assert_awaited_once_with(self.db, self.user, 7, for_update=True)
site_lock.assert_awaited_once_with(self.db, 9, for_update=True) site_lock.assert_awaited_once_with(self.db, 9, TestBatchScope.formal(), for_update=True)
site_mapping.assert_awaited_once_with(self.db, self.user, 9, for_update=True) site_mapping.assert_awaited_once_with(self.db, self.user, 9, for_update=True)
update.assert_not_awaited() update.assert_not_awaited()
add_event.assert_not_awaited() add_event.assert_not_awaited()
@@ -2491,6 +2397,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
@@ -2523,6 +2430,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
async def read_key( async def read_key(
_db: AsyncSession, _db: AsyncSession,
_key: str, _key: str,
_scope: TestBatchScope,
*, *,
warehouse_ids: list[int] | None = None, warehouse_ids: list[int] | None = None,
site_ids: list[int] | None = None, site_ids: list[int] | None = None,
@@ -2584,6 +2492,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
12, 12,
self.command, self.command,
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
@@ -2595,12 +2504,14 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
unittest.mock.call( unittest.mock.call(
self.db, self.db,
'confirm-key', 'confirm-key',
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
), ),
unittest.mock.call( unittest.mock.call(
self.db, self.db,
'confirm-key', 'confirm-key',
TestBatchScope.formal(),
warehouse_ids=[7], warehouse_ids=[7],
site_ids=[9], site_ids=[9],
), ),
@@ -2667,7 +2578,9 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised, self.assertRaises(PermissionException) as raised,
): ):
await ShipmentService.confirm_shipment_services(self.db, 12, self.command, self.user) await ShipmentService.confirm_shipment_services(
self.db, 12, self.command, self.user, TestBatchScope.formal()
)
self.assertEqual('无权访问该发货单', raised.exception.message) self.assertEqual('无权访问该发货单', raised.exception.message)
self.assertEqual('DRAFT', shipment.status) self.assertEqual('DRAFT', shipment.status)
@@ -2675,7 +2588,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(7, inventory.warehouse_id) self.assertEqual(7, inventory.warehouse_id)
self.assertIsNone(inventory.site_id) self.assertIsNone(inventory.site_id)
self.assertEqual(4, inventory.version) self.assertEqual(4, inventory.version)
site_lock.assert_awaited_once_with(self.db, 9, for_update=True) site_lock.assert_awaited_once_with(self.db, 9, TestBatchScope.formal(), for_update=True)
site_mapping.assert_awaited_once_with(self.db, self.user, 9, for_update=True) site_mapping.assert_awaited_once_with(self.db, self.user, 9, for_update=True)
transition.assert_not_called() transition.assert_not_called()
update.assert_not_awaited() update.assert_not_awaited()
@@ -2690,7 +2603,11 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
update = AsyncMock() update = AsyncMock()
add_event = AsyncMock() add_event = AsyncMock()
async def get_filling(_db: AsyncSession, filling_id: int) -> BizFillingRecord: async def get_filling(
_db: AsyncSession,
filling_id: int,
_scope: TestBatchScope,
) -> BizFillingRecord:
return fillings[filling_id] return fillings[filling_id]
transition = MagicMock(side_effect=[None, ValueError('second pallet invalid')]) transition = MagicMock(side_effect=[None, ValueError('second pallet invalid')])
@@ -2747,7 +2664,9 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaisesRegex(ValueError, 'second pallet invalid'), self.assertRaisesRegex(ValueError, 'second pallet invalid'),
): ):
await ShipmentService.confirm_shipment_services(self.db, 12, self.command, self.user) await ShipmentService.confirm_shipment_services(
self.db, 12, self.command, self.user, TestBatchScope.formal()
)
self.assertEqual('DRAFT', shipment.status) self.assertEqual('DRAFT', shipment.status)
self.assertTrue(all(item.active_filling_id is None and item.package_qty is None for item in shipment.items)) self.assertTrue(all(item.active_filling_id is None and item.package_qty is None for item in shipment.items))
@@ -31,6 +31,7 @@ from module_business.dao.inventory_dao import InventoryDao
from module_business.dao.master_dao import MasterDao from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao from module_business.dao.operation_dao import OperationDao
from module_business.domain.enums import PalletEventType, PalletFlowState from module_business.domain.enums import PalletEventType, PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizProductionLine from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizProductionLine
from module_business.entity.do.operation_do import ( from module_business.entity.do.operation_do import (
BizFillingRecord, BizFillingRecord,
@@ -48,6 +49,7 @@ from module_business.entity.vo.operation_vo import (
UsageSuccessResponseModel, UsageSuccessResponseModel,
) )
from module_business.service.access_service import AccessService from module_business.service.access_service import AccessService
from tests.module_business.test_batch_fixtures import make_test_scope
SERVICE_SPEC = importlib.util.find_spec('module_business.service.usage_service') SERVICE_SPEC = importlib.util.find_spec('module_business.service.usage_service')
CONTROLLER_SPEC = importlib.util.find_spec('module_business.controller.usage_controller') CONTROLLER_SPEC = importlib.util.find_spec('module_business.controller.usage_controller')
@@ -223,6 +225,17 @@ class TestUsageBootstrap(unittest.TestCase):
@unittest.skipUnless(UsageService is not None, 'UsageService 尚未实现') @unittest.skipUnless(UsageService is not None, 'UsageService 尚未实现')
class TestUsagePublicContract(unittest.TestCase): class TestUsagePublicContract(unittest.TestCase):
def test_public_business_methods_require_explicit_test_scope(self) -> None:
for method_name in (
'get_usage_list_services',
'usage_detail_services',
'get_line_options_services',
'scan_usage_pallet_services',
'record_usage_services',
):
with self.subTest(method=method_name):
self.assertIn('test_scope', inspect.signature(getattr(UsageService, method_name)).parameters)
def test_service_exposes_complete_usage_workflow(self) -> None: def test_service_exposes_complete_usage_workflow(self) -> None:
expected = { expected = {
'get_usage_list_services', 'get_usage_list_services',
@@ -235,11 +248,12 @@ class TestUsagePublicContract(unittest.TestCase):
def test_record_signature_and_types_are_exact(self) -> None: def test_record_signature_and_types_are_exact(self) -> None:
signature = inspect.signature(UsageService.record_usage_services) signature = inspect.signature(UsageService.record_usage_services)
self.assertEqual(('query_db', 'page_object', 'current_user'), tuple(signature.parameters)) self.assertEqual(('query_db', 'page_object', 'current_user', 'test_scope'), tuple(signature.parameters))
hints = get_type_hints(UsageService.record_usage_services) hints = get_type_hints(UsageService.record_usage_services)
self.assertIs(AsyncSession, hints['query_db']) self.assertIs(AsyncSession, hints['query_db'])
self.assertIs(UsageCreateModel, hints['page_object']) self.assertIs(UsageCreateModel, hints['page_object'])
self.assertIs(CurrentUserModel, hints['current_user']) self.assertIs(CurrentUserModel, hints['current_user'])
self.assertIs(TestBatchScope, hints['test_scope'])
self.assertIs(CrudResponseModel, hints['return']) self.assertIs(CrudResponseModel, hints['return'])
def test_request_hash_is_stable_normalized_and_excludes_idempotency_key(self) -> None: def test_request_hash_is_stable_normalized_and_excludes_idempotency_key(self) -> None:
@@ -319,9 +333,15 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[11, 9, 9])), patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[11, 9, 9])),
patch.object(OperationDao, 'get_usage_list', new=get_list), patch.object(OperationDao, 'get_usage_list', new=get_list),
): ):
result = await UsageService.get_usage_list_services(self.db, query, self.user) result = await UsageService.get_usage_list_services(self.db, query, self.user, TestBatchScope.formal())
get_list.assert_awaited_once_with(self.db, query, True, site_ids=[9, 11]) get_list.assert_awaited_once_with(
self.db,
query,
site_ids=[9, 11],
scope=TestBatchScope.formal(),
is_page=True,
)
self.assertEqual(1, result.total) self.assertEqual(1, result.total)
self.assertIsInstance(result.rows[0], UsageModel) self.assertIsInstance(result.rows[0], UsageModel)
@@ -337,6 +357,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
UsagePageQueryModel(pageNum=2, pageSize=7), UsagePageQueryModel(pageNum=2, pageSize=7),
self.user, self.user,
TestBatchScope.formal(),
is_page=is_page, is_page=is_page,
) )
@@ -360,6 +381,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
UsagePageQueryModel(siteId=10), UsagePageQueryModel(siteId=10),
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertEqual('无权访问该客户现场', raised.exception.message) self.assertEqual('无权访问该客户现场', raised.exception.message)
@@ -377,11 +399,18 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
query, query,
make_user(1), make_user(1),
TestBatchScope.formal(),
is_page=False, is_page=False,
) )
get_scope.assert_not_awaited() get_scope.assert_not_awaited()
get_list.assert_awaited_once_with(self.db, query, False, site_ids=None) get_list.assert_awaited_once_with(
self.db,
query,
site_ids=None,
scope=TestBatchScope.formal(),
is_page=False,
)
self.assertEqual([], result) self.assertEqual([], result)
async def test_detail_is_scoped_and_hides_missing_but_admin_gets_precise_error(self) -> None: async def test_detail_is_scoped_and_hides_missing_but_admin_gets_precise_error(self) -> None:
@@ -396,9 +425,14 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(OperationDao, 'get_usage_by_id', new=get_detail), patch.object(OperationDao, 'get_usage_by_id', new=get_detail),
self.assertRaises(expected_exception) as raised, self.assertRaises(expected_exception) as raised,
): ):
await UsageService.usage_detail_services(self.db, 61, user) await UsageService.usage_detail_services(self.db, 61, user, TestBatchScope.formal())
get_detail.assert_awaited_once_with(self.db, 61, site_ids=expected_scope) get_detail.assert_awaited_once_with(
self.db,
61,
TestBatchScope.formal(),
site_ids=expected_scope,
)
self.assertEqual(expected_message, raised.exception.message) self.assertEqual(expected_message, raised.exception.message)
async def test_line_options_require_site_access_and_only_return_enabled_lines_for_site(self) -> None: async def test_line_options_require_site_access_and_only_return_enabled_lines_for_site(self) -> None:
@@ -415,14 +449,15 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=make_site())), patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=make_site())),
patch.object(MasterDao, 'list_line', new=list_line), patch.object(MasterDao, 'list_line', new=list_line),
): ):
result = await UsageService.get_line_options_services(self.db, 9, self.user) result = await UsageService.get_line_options_services(self.db, 9, self.user, TestBatchScope.formal())
access.assert_awaited_once_with(self.db, self.user, 9) access.assert_awaited_once_with(self.db, self.user, 9)
query = list_line.await_args.args[1] query = list_line.await_args.args[1]
self.assertIsInstance(query, LinePageQueryModel) self.assertIsInstance(query, LinePageQueryModel)
self.assertEqual(9, query.site_id) self.assertEqual(9, query.site_id)
self.assertEqual('0', query.status) self.assertEqual('0', query.status)
self.assertFalse(list_line.await_args.args[2]) self.assertEqual(TestBatchScope.formal(), list_line.await_args.args[2])
self.assertFalse(list_line.await_args.args[3])
self.assertEqual([3, 6], [option.id for option in result]) self.assertEqual([3, 6], [option.id for option in result])
self.assertTrue(all(isinstance(option, BusinessOptionModel) for option in result)) self.assertTrue(all(isinstance(option, BusinessOptionModel) for option in result))
@@ -439,7 +474,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'list_line', new=list_line), patch.object(MasterDao, 'list_line', new=list_line),
self.assertRaises(PermissionException), self.assertRaises(PermissionException),
): ):
await UsageService.get_line_options_services(self.db, 9, self.user) await UsageService.get_line_options_services(self.db, 9, self.user, TestBatchScope.formal())
get_site.assert_not_awaited() get_site.assert_not_awaited()
list_line.assert_not_awaited() list_line.assert_not_awaited()
@@ -466,10 +501,10 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock), patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock),
patch.object(OperationDao, 'get_active_filling_for_update', new=filling_lock), patch.object(OperationDao, 'get_active_filling_for_update', new=filling_lock),
): ):
result = await UsageService.scan_usage_pallet_services(self.db, 'qr-2', self.user) result = await UsageService.scan_usage_pallet_services(self.db, 'qr-2', self.user, TestBatchScope.formal())
access.assert_awaited_once_with(self.db, self.user, 9) access.assert_awaited_once_with(self.db, self.user, 9)
get_filling.assert_awaited_once_with(self.db, 31) get_filling.assert_awaited_once_with(self.db, 31, TestBatchScope.formal())
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
pallet_lock.assert_not_awaited() pallet_lock.assert_not_awaited()
filling_lock.assert_not_awaited() filling_lock.assert_not_awaited()
@@ -489,7 +524,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
self.assertRaises(PermissionException) as missing, self.assertRaises(PermissionException) as missing,
): ):
await UsageService.scan_usage_pallet_services(self.db, 'missing', self.user) await UsageService.scan_usage_pallet_services(self.db, 'missing', self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', missing.exception.message) self.assertEqual('无权访问该托盘', missing.exception.message)
with ( with (
@@ -497,7 +532,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=make_inventory(site_id=None))), patch.object(MasterDao, 'get_pallet_inventory', new=AsyncMock(return_value=make_inventory(site_id=None))),
self.assertRaises(PermissionException) as non_site, self.assertRaises(PermissionException) as non_site,
): ):
await UsageService.scan_usage_pallet_services(self.db, 'qr-2', self.user) await UsageService.scan_usage_pallet_services(self.db, 'qr-2', self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', non_site.exception.message) self.assertEqual('无权访问该托盘', non_site.exception.message)
with ( with (
@@ -510,7 +545,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
), ),
self.assertRaises(PermissionException) as unauthorized, self.assertRaises(PermissionException) as unauthorized,
): ):
await UsageService.scan_usage_pallet_services(self.db, 'qr-2', self.user) await UsageService.scan_usage_pallet_services(self.db, 'qr-2', self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', unauthorized.exception.message) self.assertEqual('无权访问该托盘', unauthorized.exception.message)
async def test_scan_admin_keeps_precise_missing_error(self) -> None: async def test_scan_admin_keeps_precise_missing_error(self) -> None:
@@ -518,7 +553,7 @@ class TestUsageReadServices(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await UsageService.scan_usage_pallet_services(self.db, 'missing', make_user(1)) await UsageService.scan_usage_pallet_services(self.db, 'missing', make_user(1), TestBatchScope.formal())
self.assertEqual('托盘不存在', raised.exception.message) self.assertEqual('托盘不存在', raised.exception.message)
@@ -644,6 +679,7 @@ class TestUsageControllerResponse(unittest.IsolatedAsyncioTestCase):
db = MagicMock(spec=AsyncSession) db = MagicMock(spec=AsyncSession)
user = make_user() user = make_user()
page = make_page() page = make_page()
test_scope = make_test_scope(9)
service = AsyncMock( service = AsyncMock(
return_value=CrudResponseModel( return_value=CrudResponseModel(
is_success=True, is_success=True,
@@ -661,9 +697,10 @@ class TestUsageControllerResponse(unittest.IsolatedAsyncioTestCase):
add_usage=page, add_usage=page,
query_db=db, query_db=db,
current_user=user, current_user=user,
test_scope=test_scope,
) )
service.assert_awaited_once_with(db, page, user) service.assert_awaited_once_with(db, page, user, test_scope)
payload = json.loads(response.body) payload = json.loads(response.body)
self.assertEqual('使用成功', payload['msg']) self.assertEqual('使用成功', payload['msg'])
self.assertIn('data', payload) self.assertIn('data', payload)
@@ -738,12 +775,13 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
async def read_line( async def read_line(
db: AsyncSession, db: AsyncSession,
line_id: int, line_id: int,
scope: TestBatchScope,
*, *,
for_update: bool = False, for_update: bool = False,
) -> BizProductionLine | None: ) -> BizProductionLine | None:
if for_update: if for_update:
return await line_lock(db, line_id, for_update=True) return await line_lock(db, line_id, scope, for_update=True)
return await preflight_line_read(db, line_id) return await preflight_line_read(db, line_id, scope)
stack.enter_context(patch.object(AccessDao, 'list_user_site_ids', new=scope_read)) stack.enter_context(patch.object(AccessDao, 'list_user_site_ids', new=scope_read))
stack.enter_context(patch.object(AccessService, 'assert_site_access', new=site_access)) stack.enter_context(patch.object(AccessService, 'assert_site_access', new=site_access))
@@ -884,7 +922,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
patch('module_business.service.usage_service.datetime') as mocked_datetime, patch('module_business.service.usage_service.datetime') as mocked_datetime,
): ):
mocked_datetime.now.return_value = self.fixed_time mocked_datetime.now.return_value = self.fixed_time
result = await UsageService.record_usage_services(self.db, self.page, self.user) result = await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual( self.assertEqual(
[ [
@@ -987,6 +1025,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
make_page(used_packages=3), make_page(used_packages=3),
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
@@ -1028,6 +1067,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
make_page(used_packages=2), make_page(used_packages=2),
self.user, self.user,
TestBatchScope.formal(),
) )
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
self.assertEqual(4, same_inventory.current_line_id) self.assertEqual(4, same_inventory.current_line_id)
@@ -1040,7 +1080,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
) )
stack, mocks = self._patch_record_path(inventory=other_inventory) stack, mocks = self._patch_record_path(inventory=other_inventory)
with stack, self.assertRaises(ServiceException) as raised: with stack, self.assertRaises(ServiceException) as raised:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertIn('已绑定其他产线', raised.exception.message) self.assertIn('已绑定其他产线', raised.exception.message)
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
mocks['add_event'].assert_not_awaited() mocks['add_event'].assert_not_awaited()
@@ -1060,7 +1100,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
mocks['preflight_line_read'].return_value = None mocks['preflight_line_read'].return_value = None
mocks['line_lock'].return_value = None mocks['line_lock'].return_value = None
with stack, self.assertRaises(ServiceException) as raised: with stack, self.assertRaises(ServiceException) as raised:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(message, raised.exception.message) self.assertEqual(message, raised.exception.message)
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
mocks['add_event'].assert_not_awaited() mocks['add_event'].assert_not_awaited()
@@ -1091,7 +1131,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
self.db.reset_mock() self.db.reset_mock()
stack, mocks = self._patch_record_path(inventory=inventory) stack, mocks = self._patch_record_path(inventory=inventory)
with stack, self.assertRaises(ServiceException) as raised: with stack, self.assertRaises(ServiceException) as raised:
await UsageService.record_usage_services(self.db, page, self.user) await UsageService.record_usage_services(self.db, page, self.user, TestBatchScope.formal())
self.assertEqual(message, raised.exception.message) self.assertEqual(message, raised.exception.message)
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
mocks['add_event'].assert_not_awaited() mocks['add_event'].assert_not_awaited()
@@ -1102,7 +1142,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
access = AsyncMock(side_effect=PermissionException(data='', message='无权访问该客户现场')) access = AsyncMock(side_effect=PermissionException(data='', message='无权访问该客户现场'))
stack, mocks = self._patch_record_path(access=access) stack, mocks = self._patch_record_path(access=access)
with stack, self.assertRaises(PermissionException) as raised: with stack, self.assertRaises(PermissionException) as raised:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
mocks['inventory_lock'].assert_awaited_once() mocks['inventory_lock'].assert_awaited_once()
@@ -1127,10 +1167,15 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
access = AsyncMock(side_effect=access_check) access = AsyncMock(side_effect=access_check)
stack, mocks = self._patch_record_path(site=make_site(status='1'), access=access) stack, mocks = self._patch_record_path(site=make_site(status='1'), access=access)
with stack, self.assertRaises(PermissionException) as raised: with stack, self.assertRaises(PermissionException) as raised:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', raised.exception.message) self.assertEqual('无权访问该托盘', raised.exception.message)
mocks['site_lock'].assert_awaited_once_with(self.db, 9, for_update=True) mocks['site_lock'].assert_awaited_once_with(
self.db,
9,
TestBatchScope.formal(),
for_update=True,
)
access.assert_awaited_with(self.db, self.user, 9, for_update=True) access.assert_awaited_with(self.db, self.user, 9, for_update=True)
mocks['line_lock'].assert_not_awaited() mocks['line_lock'].assert_not_awaited()
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
@@ -1144,7 +1189,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
if site is None: if site is None:
mocks['site_lock'].return_value = None mocks['site_lock'].return_value = None
with stack, self.assertRaises(ServiceException) as raised: with stack, self.assertRaises(ServiceException) as raised:
await UsageService.record_usage_services(self.db, self.page, make_user(1)) await UsageService.record_usage_services(self.db, self.page, make_user(1), TestBatchScope.formal())
self.assertEqual(message, raised.exception.message) self.assertEqual(message, raised.exception.message)
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -1162,7 +1207,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
self.db.reset_mock() self.db.reset_mock()
stack, mocks = self._patch_record_path(**overrides) stack, mocks = self._patch_record_path(**overrides)
with stack, self.assertRaises(ServiceException): with stack, self.assertRaises(ServiceException):
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
mocks['update_filling'].assert_not_awaited() mocks['update_filling'].assert_not_awaited()
mocks['add_event'].assert_not_awaited() mocks['add_event'].assert_not_awaited()
@@ -1173,7 +1218,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
inventory = make_inventory(version=-1) inventory = make_inventory(version=-1)
stack, mocks = self._patch_record_path(inventory=inventory) stack, mocks = self._patch_record_path(inventory=inventory)
with stack, self.assertRaises(ServiceException) as raised: with stack, self.assertRaises(ServiceException) as raised:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('托盘库存版本无效', raised.exception.message) self.assertEqual('托盘库存版本无效', raised.exception.message)
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
@@ -1188,7 +1233,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
self.assertRaises(PermissionException) as normal_missing, self.assertRaises(PermissionException) as normal_missing,
): ):
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', normal_missing.exception.message) self.assertEqual('无权访问该托盘', normal_missing.exception.message)
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
@@ -1199,14 +1244,14 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=None)),
self.assertRaises(ServiceException) as admin_missing, self.assertRaises(ServiceException) as admin_missing,
): ):
await UsageService.record_usage_services(self.db, self.page, make_user(1)) await UsageService.record_usage_services(self.db, self.page, make_user(1), TestBatchScope.formal())
self.assertEqual('托盘不存在', admin_missing.exception.message) self.assertEqual('托盘不存在', admin_missing.exception.message)
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
self.db.reset_mock() self.db.reset_mock()
stack, mocks = self._patch_record_path(inventory=make_inventory(site_id=None)) stack, mocks = self._patch_record_path(inventory=make_inventory(site_id=None))
with stack, self.assertRaises(PermissionException) as non_site: with stack, self.assertRaises(PermissionException) as non_site:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('无权访问该托盘', non_site.exception.message) self.assertEqual('无权访问该托盘', non_site.exception.message)
mocks['pallet_lock'].assert_not_awaited() mocks['pallet_lock'].assert_not_awaited()
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
@@ -1329,14 +1374,15 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock), patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock),
patch.object(OperationDao, 'add_usage_record', new=AsyncMock()) as add_usage, patch.object(OperationDao, 'add_usage_record', new=AsyncMock()) as add_usage,
): ):
result = await UsageService.record_usage_services(self.db, self.page, self.user) result = await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
get_key.assert_awaited_once_with( get_key.assert_awaited_once_with(
self.db, self.db,
'usage-key', 'usage-key',
TestBatchScope.formal(),
site_ids=[9], site_ids=[9],
) )
site_lock.assert_awaited_once_with(self.db, 9, for_update=True) site_lock.assert_awaited_once_with(self.db, 9, TestBatchScope.formal(), for_update=True)
access.assert_awaited_once_with(self.db, self.user, 9, for_update=True) access.assert_awaited_once_with(self.db, self.user, 9, for_update=True)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
add_usage.assert_not_awaited() add_usage.assert_not_awaited()
@@ -1367,10 +1413,15 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=make_site())), patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=make_site())),
patch.object(AccessService, 'assert_site_access', new=AsyncMock()), patch.object(AccessService, 'assert_site_access', new=AsyncMock()),
): ):
result = await UsageService.record_usage_services(self.db, self.page, user) result = await UsageService.record_usage_services(self.db, self.page, user, TestBatchScope.formal())
line_read.assert_awaited_once_with(self.db, 4) line_read.assert_awaited_once_with(self.db, 4, TestBatchScope.formal())
get_key.assert_awaited_once_with(self.db, 'usage-key', site_ids=[9]) get_key.assert_awaited_once_with(
self.db,
'usage-key',
TestBatchScope.formal(),
site_ids=[9],
)
self.assertEqual(61, result.result.usage_id) self.assertEqual(61, result.result.usage_id)
if user.user and user.user.admin: if user.user and user.user.admin:
scope_read.assert_not_awaited() scope_read.assert_not_awaited()
@@ -1384,7 +1435,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock()) as inventory_lock, patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock()) as inventory_lock,
self.assertRaises(ServiceException) as raised, self.assertRaises(ServiceException) as raised,
): ):
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertIn('幂等键', raised.exception.message) self.assertIn('幂等键', raised.exception.message)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
@@ -1401,7 +1452,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
patch.object(AccessService, 'assert_site_access', new=access), patch.object(AccessService, 'assert_site_access', new=access),
self.assertRaises(PermissionException), self.assertRaises(PermissionException),
): ):
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
access.assert_awaited_once_with(self.db, self.user, 9, for_update=True) access.assert_awaited_once_with(self.db, self.user, 9, for_update=True)
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -1460,6 +1511,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
self.db, self.db,
self.page, self.page,
self.user, self.user,
TestBatchScope.formal(),
) )
except ServiceException as exc: except ServiceException as exc:
self.fail(f'精确并发重试被旧库存状态提前拒绝:{exc.message}') self.fail(f'精确并发重试被旧库存状态提前拒绝:{exc.message}')
@@ -1495,7 +1547,12 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
mocks['filling_lock'].assert_awaited_once() mocks['filling_lock'].assert_awaited_once()
else: else:
mocks['filling_lock'].assert_not_awaited() mocks['filling_lock'].assert_not_awaited()
mocks['site_lock'].assert_awaited_once_with(self.db, 9, for_update=True) mocks['site_lock'].assert_awaited_once_with(
self.db,
9,
TestBatchScope.formal(),
for_update=True,
)
mocks['line_lock'].assert_not_awaited() mocks['line_lock'].assert_not_awaited()
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
mocks['add_event'].assert_not_awaited() mocks['add_event'].assert_not_awaited()
@@ -1541,7 +1598,9 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
) )
with stack: with stack:
try: try:
result = await UsageService.record_usage_services(self.db, self.page, self.user) result = await UsageService.record_usage_services(
self.db, self.page, self.user, TestBatchScope.formal()
)
except ServiceException as exc: except ServiceException as exc:
self.fail(f'锁等待期间新增授权后未使用 fresh scope:{exc.message}') self.fail(f'锁等待期间新增授权后未使用 fresh scope:{exc.message}')
@@ -1590,7 +1649,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
) )
with stack: with stack:
try: try:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
except PermissionException as exc: except PermissionException as exc:
self.assertEqual('无权访问该托盘', exc.message) self.assertEqual('无权访问该托盘', exc.message)
else: else:
@@ -1653,13 +1712,13 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
self.db.rollback.side_effect = rollback self.db.rollback.side_effect = rollback
with stack, self.assertRaises(ServiceException) as raised: with stack, self.assertRaises(ServiceException) as raised:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual('托盘二维码与库存不一致', raised.exception.message) self.assertEqual('托盘二维码与库存不一致', raised.exception.message)
self.assertTrue(scanned.expired) self.assertTrue(scanned.expired)
self.assertEqual(1, scanned.pallet_id_reads) self.assertEqual(1, scanned.pallet_id_reads)
mocks['inventory_lock'].assert_awaited_once_with(self.db, 2) mocks['inventory_lock'].assert_awaited_once_with(self.db, 2, TestBatchScope.formal())
mocks['pallet_lock'].assert_awaited_once_with(self.db, [2]) mocks['pallet_lock'].assert_awaited_once_with(self.db, [2], TestBatchScope.formal())
self.assertEqual(2, scope.await_count) self.assertEqual(2, scope.await_count)
self.assertEqual( self.assertEqual(
[{'site_ids': [9]}, {'site_ids': [9]}], [{'site_ids': [9]}, {'site_ids': [9]}],
@@ -1688,7 +1747,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
access = mocks['access'] access = mocks['access']
with stack: with stack:
try: try:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
except PermissionException as exc: except PermissionException as exc:
self.assertEqual('无权访问该使用记录', exc.message) self.assertEqual('无权访问该使用记录', exc.message)
except ServiceException as exc: except ServiceException as exc:
@@ -1701,7 +1760,12 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
[call.kwargs for call in key_read.await_args_list], [call.kwargs for call in key_read.await_args_list],
) )
self.assertEqual(2, mocks['scope_read'].await_count) self.assertEqual(2, mocks['scope_read'].await_count)
mocks['site_lock'].assert_awaited_once_with(self.db, 9, for_update=True) mocks['site_lock'].assert_awaited_once_with(
self.db,
9,
TestBatchScope.formal(),
for_update=True,
)
self.assertEqual([False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list]) self.assertEqual([False, True], [call.kwargs.get('for_update', False) for call in access.await_args_list])
mocks['line_lock'].assert_not_awaited() mocks['line_lock'].assert_not_awaited()
mocks['add_usage'].assert_not_awaited() mocks['add_usage'].assert_not_awaited()
@@ -1734,7 +1798,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
key_read = mocks['key_read'] key_read = mocks['key_read']
with stack: with stack:
try: try:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
except ServiceException as exc: except ServiceException as exc:
self.assertEqual( self.assertEqual(
'使用失败,幂等键已被其他请求使用', '使用失败,幂等键已被其他请求使用',
@@ -1768,7 +1832,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
add_usage=add_usage, add_usage=add_usage,
) )
with stack: with stack:
result = await UsageService.record_usage_services(self.db, self.page, self.user) result = await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertIsInstance(result.result, UsageSuccessResponseModel) self.assertIsInstance(result.result, UsageSuccessResponseModel)
self.assertEqual( self.assertEqual(
@@ -1800,7 +1864,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
add_usage=AsyncMock(side_effect=integrity_error()), add_usage=AsyncMock(side_effect=integrity_error()),
) )
with stack, self.assertRaises(ServiceException) as raised: with stack, self.assertRaises(ServiceException) as raised:
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertTrue('幂等' in raised.exception.message or '冲突' in raised.exception.message) self.assertTrue('幂等' in raised.exception.message or '冲突' in raised.exception.message)
self.assertEqual( self.assertEqual(
[{'site_ids': [9]}, {'site_ids': [9]}, {'site_ids': [9]}], [{'site_ids': [9]}, {'site_ids': [9]}, {'site_ids': [9]}],
@@ -1818,7 +1882,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
add_usage=AsyncMock(side_effect=integrity_error()), add_usage=AsyncMock(side_effect=integrity_error()),
) )
with stack, self.assertRaises(PermissionException): with stack, self.assertRaises(PermissionException):
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(3, scope.await_count) self.assertEqual(3, scope.await_count)
self.assertEqual(2, key_read.await_count) self.assertEqual(2, key_read.await_count)
@@ -1835,7 +1899,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
add_event=AsyncMock(side_effect=RuntimeError('event failed')), add_event=AsyncMock(side_effect=RuntimeError('event failed')),
) )
with stack, self.assertRaises(RuntimeError): with stack, self.assertRaises(RuntimeError):
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -1845,7 +1909,7 @@ class TestUsageIdempotency(unittest.IsolatedAsyncioTestCase):
patch.object(UsageService, '_build_request_hash', side_effect=RuntimeError('hash failed')), patch.object(UsageService, '_build_request_hash', side_effect=RuntimeError('hash failed')),
self.assertRaises(RuntimeError), self.assertRaises(RuntimeError),
): ):
await UsageService.record_usage_services(self.db, self.page, self.user) await UsageService.record_usage_services(self.db, self.page, self.user, TestBatchScope.formal())
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()