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