安全:隔离报表选项扫码与首页数据
This commit is contained in:
@@ -11,6 +11,8 @@ from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency
|
||||
from common.router import APIRouterPro
|
||||
from common.vo import 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.report_vo import (
|
||||
ConsumptionReportModel,
|
||||
ConsumptionReportQueryModel,
|
||||
@@ -46,8 +48,9 @@ async def get_pallet_inventory_report_list(
|
||||
query_object: Annotated[PalletInventoryReportQueryModel, Query()],
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
|
||||
) -> Response:
|
||||
result = await ReportService.get_pallet_inventory_list_services(query_db, query_object, current_user)
|
||||
result = await ReportService.get_pallet_inventory_list_services(query_db, query_object, current_user, test_scope)
|
||||
return ResponseUtil.success(model_content=result)
|
||||
|
||||
|
||||
@@ -62,8 +65,9 @@ async def get_filling_report_list(
|
||||
query_object: Annotated[FillingReportQueryModel, Query()],
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
|
||||
) -> Response:
|
||||
result = await ReportService.get_filling_report_list_services(query_db, query_object, current_user)
|
||||
result = await ReportService.get_filling_report_list_services(query_db, query_object, current_user, test_scope)
|
||||
return ResponseUtil.success(model_content=result)
|
||||
|
||||
|
||||
@@ -78,8 +82,9 @@ async def get_shipment_report_list(
|
||||
query_object: Annotated[ShipmentReportQueryModel, Query()],
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
|
||||
) -> Response:
|
||||
result = await ReportService.get_shipment_report_list_services(query_db, query_object, current_user)
|
||||
result = await ReportService.get_shipment_report_list_services(query_db, query_object, current_user, test_scope)
|
||||
return ResponseUtil.success(model_content=result)
|
||||
|
||||
|
||||
@@ -94,8 +99,9 @@ async def get_consumption_report_list(
|
||||
query_object: Annotated[ConsumptionReportQueryModel, Query()],
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
|
||||
) -> Response:
|
||||
result = await ReportService.get_consumption_report_list_services(query_db, query_object, current_user)
|
||||
result = await ReportService.get_consumption_report_list_services(query_db, query_object, current_user, test_scope)
|
||||
return ResponseUtil.success(model_content=result)
|
||||
|
||||
|
||||
@@ -110,8 +116,9 @@ async def get_return_report_list(
|
||||
query_object: Annotated[ReturnReportQueryModel, Query()],
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
|
||||
) -> Response:
|
||||
result = await ReportService.get_return_report_list_services(query_db, query_object, current_user)
|
||||
result = await ReportService.get_return_report_list_services(query_db, query_object, current_user, test_scope)
|
||||
return ResponseUtil.success(model_content=result)
|
||||
|
||||
|
||||
@@ -126,6 +133,7 @@ async def get_pallet_trace_report_list(
|
||||
query_object: Annotated[PalletTraceReportQueryModel, Query()],
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
test_scope: Annotated[TestBatchScope, TestBatchScopeDependency()],
|
||||
) -> Response:
|
||||
result = await ReportService.get_pallet_trace_list_services(query_db, query_object, current_user)
|
||||
result = await ReportService.get_pallet_trace_list_services(query_db, query_object, current_user, test_scope)
|
||||
return ResponseUtil.success(model_content=result)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from module_business.entity.do.master_do import BizUserSite, BizUserWarehouse
|
||||
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
|
||||
from module_business.entity.do.master_do import BizCustomerSite, BizUserSite, BizUserWarehouse, BizWarehouse
|
||||
|
||||
|
||||
class AccessDao:
|
||||
@@ -48,16 +49,44 @@ class AccessDao:
|
||||
return mapped_user_id is not None
|
||||
|
||||
@classmethod
|
||||
async def list_user_warehouse_ids(cls, db: AsyncSession, user_id: int) -> list[int]:
|
||||
"""获取用户关联的仓库 ID 列表。"""
|
||||
result = await db.execute(select(BizUserWarehouse.warehouse_id).where(BizUserWarehouse.user_id == user_id))
|
||||
async def list_user_warehouse_ids(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
scope: TestBatchScope | None = None,
|
||||
) -> list[int]:
|
||||
"""获取用户关联且处于当前批次作用域的仓库 ID 列表。"""
|
||||
conditions = [BizUserWarehouse.user_id == user_id]
|
||||
if scope is not None:
|
||||
conditions.append(batch_scope_condition(BizWarehouse.test_batch_id, scope))
|
||||
statement = (
|
||||
select(BizUserWarehouse.warehouse_id)
|
||||
.select_from(BizUserWarehouse)
|
||||
.join(BizWarehouse, BizWarehouse.warehouse_id == BizUserWarehouse.warehouse_id)
|
||||
.where(*conditions)
|
||||
)
|
||||
result = await db.execute(statement)
|
||||
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def list_user_site_ids(cls, db: AsyncSession, user_id: int) -> list[int]:
|
||||
"""获取用户关联的客户现场 ID 列表。"""
|
||||
result = await db.execute(select(BizUserSite.site_id).where(BizUserSite.user_id == user_id))
|
||||
async def list_user_site_ids(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
scope: TestBatchScope | None = None,
|
||||
) -> list[int]:
|
||||
"""获取用户关联且处于当前批次作用域的客户现场 ID 列表。"""
|
||||
conditions = [BizUserSite.user_id == user_id]
|
||||
if scope is not None:
|
||||
conditions.append(batch_scope_condition(BizCustomerSite.test_batch_id, scope))
|
||||
statement = (
|
||||
select(BizUserSite.site_id)
|
||||
.select_from(BizUserSite)
|
||||
.join(BizCustomerSite, BizCustomerSite.site_id == BizUserSite.site_id)
|
||||
.where(*conditions)
|
||||
)
|
||||
result = await db.execute(statement)
|
||||
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
from common.vo import PageModel
|
||||
from module_admin.entity.do.user_do import SysUser
|
||||
from module_business.domain.enums import DocumentStatus, PalletEventType
|
||||
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
|
||||
from module_business.entity.do.master_do import (
|
||||
BizCustomer,
|
||||
BizCustomerSite,
|
||||
@@ -74,6 +75,15 @@ class ReportDao:
|
||||
normalized_ids = sorted(set(scope_ids))
|
||||
return column.in_(normalized_ids) if normalized_ids else false()
|
||||
|
||||
@staticmethod
|
||||
def _joined_scope_condition(
|
||||
id_column: InstrumentedAttribute,
|
||||
scope_column: InstrumentedAttribute,
|
||||
scope: TestBatchScope,
|
||||
) -> ColumnElement[bool]:
|
||||
"""保留空的可选 join,同时拒绝已存在的跨批次关联记录。"""
|
||||
return or_(id_column.is_(None), batch_scope_condition(scope_column, scope))
|
||||
|
||||
@classmethod
|
||||
def _or_scope_conditions(
|
||||
cls,
|
||||
@@ -140,6 +150,7 @@ class ReportDao:
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
query_object: PalletInventoryReportQueryModel,
|
||||
scope: TestBatchScope,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
site_ids: list[int] | None = None,
|
||||
) -> PageModel[PalletInventoryReportModel]:
|
||||
@@ -148,9 +159,19 @@ class ReportDao:
|
||||
current_line = aliased(BizProductionLine, name='current_line')
|
||||
active_filling = aliased(BizFillingRecord, name='active_filling')
|
||||
|
||||
conditions = cls._or_scope_conditions(
|
||||
(BizPalletInventory.warehouse_id, warehouse_ids),
|
||||
(BizPalletInventory.site_id, site_ids),
|
||||
conditions = [
|
||||
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
cls._joined_scope_condition(active_filling.filling_id, active_filling.test_batch_id, scope),
|
||||
cls._joined_scope_condition(current_warehouse.warehouse_id, current_warehouse.test_batch_id, scope),
|
||||
cls._joined_scope_condition(current_site.site_id, current_site.test_batch_id, scope),
|
||||
cls._joined_scope_condition(current_line.line_id, current_line.test_batch_id, scope),
|
||||
]
|
||||
conditions.extend(
|
||||
cls._or_scope_conditions(
|
||||
(BizPalletInventory.warehouse_id, warehouse_ids),
|
||||
(BizPalletInventory.site_id, site_ids),
|
||||
)
|
||||
)
|
||||
if query_object.pallet_code:
|
||||
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
|
||||
@@ -209,10 +230,15 @@ class ReportDao:
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
query_object: FillingReportQueryModel,
|
||||
scope: TestBatchScope,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
) -> PageModel[FillingReportModel]:
|
||||
operator_user = aliased(SysUser, name='operator_user')
|
||||
conditions = []
|
||||
conditions = [
|
||||
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
cls._joined_scope_condition(BizWarehouse.warehouse_id, BizWarehouse.test_batch_id, scope),
|
||||
]
|
||||
warehouse_scope = cls._scope_condition(BizFillingRecord.warehouse_id, warehouse_ids)
|
||||
if warehouse_scope is not None:
|
||||
conditions.append(warehouse_scope)
|
||||
@@ -264,13 +290,22 @@ class ReportDao:
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
query_object: ShipmentReportQueryModel,
|
||||
scope: TestBatchScope,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
site_ids: list[int] | None = None,
|
||||
) -> PageModel[ShipmentReportModel]:
|
||||
source_warehouse = aliased(BizWarehouse, name='source_warehouse')
|
||||
target_site = aliased(BizCustomerSite, name='target_site')
|
||||
target_customer = aliased(BizCustomer, name='target_customer')
|
||||
conditions = [BizShipmentOrder.status == DocumentStatus.CONFIRMED.value]
|
||||
conditions = [
|
||||
batch_scope_condition(BizShipmentOrder.test_batch_id, scope),
|
||||
batch_scope_condition(BizShipmentItem.test_batch_id, scope),
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
cls._joined_scope_condition(source_warehouse.warehouse_id, source_warehouse.test_batch_id, scope),
|
||||
cls._joined_scope_condition(target_site.site_id, target_site.test_batch_id, scope),
|
||||
cls._joined_scope_condition(target_customer.customer_id, target_customer.test_batch_id, scope),
|
||||
BizShipmentOrder.status == DocumentStatus.CONFIRMED.value,
|
||||
]
|
||||
warehouse_scope = cls._scope_condition(BizShipmentOrder.source_warehouse_id, warehouse_ids)
|
||||
site_scope = cls._scope_condition(BizShipmentOrder.target_site_id, site_ids)
|
||||
if warehouse_scope is not None:
|
||||
@@ -334,10 +369,18 @@ class ReportDao:
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
query_object: ConsumptionReportQueryModel,
|
||||
scope: TestBatchScope,
|
||||
site_ids: list[int] | None = None,
|
||||
) -> PageModel[ConsumptionReportModel]:
|
||||
operator_user = aliased(SysUser, name='operator_user')
|
||||
conditions = []
|
||||
conditions = [
|
||||
batch_scope_condition(BizUsageRecord.test_batch_id, scope),
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
batch_scope_condition(BizFillingRecord.test_batch_id, scope),
|
||||
batch_scope_condition(BizCustomerSite.test_batch_id, scope),
|
||||
batch_scope_condition(BizProductionLine.test_batch_id, scope),
|
||||
cls._joined_scope_condition(BizCustomer.customer_id, BizCustomer.test_batch_id, scope),
|
||||
]
|
||||
site_scope = cls._scope_condition(BizUsageRecord.site_id, site_ids)
|
||||
if site_scope is not None:
|
||||
conditions.append(site_scope)
|
||||
@@ -400,14 +443,24 @@ class ReportDao:
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
query_object: ReturnReportQueryModel,
|
||||
scope: TestBatchScope,
|
||||
site_ids: list[int] | None = None,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
) -> PageModel[ReturnReportModel]:
|
||||
source_site = aliased(BizCustomerSite, name='source_site')
|
||||
target_warehouse = aliased(BizWarehouse, name='target_warehouse')
|
||||
conditions = cls._or_scope_conditions(
|
||||
(BizReturnOrder.source_site_id, site_ids),
|
||||
(BizReturnOrder.target_warehouse_id, warehouse_ids),
|
||||
conditions = [
|
||||
batch_scope_condition(BizReturnOrder.test_batch_id, scope),
|
||||
batch_scope_condition(BizReturnItem.test_batch_id, scope),
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
cls._joined_scope_condition(source_site.site_id, source_site.test_batch_id, scope),
|
||||
cls._joined_scope_condition(target_warehouse.warehouse_id, target_warehouse.test_batch_id, scope),
|
||||
]
|
||||
conditions.extend(
|
||||
cls._or_scope_conditions(
|
||||
(BizReturnOrder.source_site_id, site_ids),
|
||||
(BizReturnOrder.target_warehouse_id, warehouse_ids),
|
||||
)
|
||||
)
|
||||
if query_object.return_no:
|
||||
conditions.append(BizReturnOrder.return_no.like(f'%{query_object.return_no}%'))
|
||||
@@ -458,6 +511,7 @@ class ReportDao:
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
query_object: PalletTraceReportQueryModel,
|
||||
scope: TestBatchScope,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
site_ids: list[int] | None = None,
|
||||
) -> PageModel[PalletTraceReportModel]:
|
||||
@@ -468,7 +522,17 @@ class ReportDao:
|
||||
before_line = aliased(BizProductionLine, name='before_line')
|
||||
after_line = aliased(BizProductionLine, name='after_line')
|
||||
operator_user = aliased(SysUser, name='operator_user')
|
||||
conditions = cls._pallet_trace_scope_conditions(warehouse_ids, site_ids)
|
||||
conditions = [
|
||||
batch_scope_condition(BizPalletEvent.test_batch_id, scope),
|
||||
batch_scope_condition(BizPallet.test_batch_id, scope),
|
||||
cls._joined_scope_condition(before_warehouse.warehouse_id, before_warehouse.test_batch_id, scope),
|
||||
cls._joined_scope_condition(after_warehouse.warehouse_id, after_warehouse.test_batch_id, scope),
|
||||
cls._joined_scope_condition(before_site.site_id, before_site.test_batch_id, scope),
|
||||
cls._joined_scope_condition(after_site.site_id, after_site.test_batch_id, scope),
|
||||
cls._joined_scope_condition(before_line.line_id, before_line.test_batch_id, scope),
|
||||
cls._joined_scope_condition(after_line.line_id, after_line.test_batch_id, scope),
|
||||
]
|
||||
conditions.extend(cls._pallet_trace_scope_conditions(warehouse_ids, site_ids))
|
||||
if query_object.pallet_code:
|
||||
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
|
||||
if query_object.event_type:
|
||||
|
||||
@@ -8,6 +8,7 @@ from exceptions.exception import ServiceException
|
||||
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||
from module_business.dao.access_dao import AccessDao
|
||||
from module_business.dao.report_dao import ReportDao
|
||||
from module_business.domain.test_batch_scope import TestBatchScope
|
||||
from module_business.entity.vo.report_vo import (
|
||||
ConsumptionReportModel,
|
||||
ConsumptionReportQueryModel,
|
||||
@@ -44,10 +45,11 @@ class ReportService:
|
||||
cls,
|
||||
query_db: AsyncSession,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> list[int] | None:
|
||||
if cls._is_admin(current_user):
|
||||
return None
|
||||
warehouse_ids = await AccessDao.list_user_warehouse_ids(query_db, cls._user_id(current_user))
|
||||
warehouse_ids = await AccessDao.list_user_warehouse_ids(query_db, cls._user_id(current_user), test_scope)
|
||||
return sorted(set(warehouse_ids))
|
||||
|
||||
@classmethod
|
||||
@@ -55,10 +57,11 @@ class ReportService:
|
||||
cls,
|
||||
query_db: AsyncSession,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> list[int] | None:
|
||||
if cls._is_admin(current_user):
|
||||
return None
|
||||
site_ids = await AccessDao.list_user_site_ids(query_db, cls._user_id(current_user))
|
||||
site_ids = await AccessDao.list_user_site_ids(query_db, cls._user_id(current_user), test_scope)
|
||||
return sorted(set(site_ids))
|
||||
|
||||
@classmethod
|
||||
@@ -66,12 +69,13 @@ class ReportService:
|
||||
cls,
|
||||
query_db: AsyncSession,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> tuple[list[int] | None, list[int] | None]:
|
||||
if cls._is_admin(current_user):
|
||||
return None, None
|
||||
return (
|
||||
await cls._warehouse_scope(query_db, current_user),
|
||||
await cls._site_scope(query_db, current_user),
|
||||
await cls._warehouse_scope(query_db, current_user, test_scope),
|
||||
await cls._site_scope(query_db, current_user, test_scope),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -95,13 +99,15 @@ class ReportService:
|
||||
query_db: AsyncSession,
|
||||
query_object: PalletInventoryReportQueryModel,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> PageModel[PalletInventoryReportModel]:
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user, test_scope)
|
||||
if warehouse_ids == [] and site_ids == []:
|
||||
return cls._empty_page(query_object)
|
||||
page = await ReportDao.get_pallet_inventory_list(
|
||||
query_db,
|
||||
query_object,
|
||||
test_scope,
|
||||
warehouse_ids=warehouse_ids,
|
||||
site_ids=site_ids,
|
||||
)
|
||||
@@ -113,13 +119,15 @@ class ReportService:
|
||||
query_db: AsyncSession,
|
||||
query_object: FillingReportQueryModel,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> PageModel[FillingReportModel]:
|
||||
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
|
||||
warehouse_ids = await cls._warehouse_scope(query_db, current_user, test_scope)
|
||||
if warehouse_ids == []:
|
||||
return cls._empty_page(query_object)
|
||||
page = await ReportDao.get_filling_report_list(
|
||||
query_db,
|
||||
query_object,
|
||||
test_scope,
|
||||
warehouse_ids=warehouse_ids,
|
||||
)
|
||||
return cls._convert_page(page, FillingReportModel)
|
||||
@@ -130,13 +138,15 @@ class ReportService:
|
||||
query_db: AsyncSession,
|
||||
query_object: ShipmentReportQueryModel,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> PageModel[ShipmentReportModel]:
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user, test_scope)
|
||||
if warehouse_ids == [] or site_ids == []:
|
||||
return cls._empty_page(query_object)
|
||||
page = await ReportDao.get_shipment_report_list(
|
||||
query_db,
|
||||
query_object,
|
||||
test_scope,
|
||||
warehouse_ids=warehouse_ids,
|
||||
site_ids=site_ids,
|
||||
)
|
||||
@@ -148,13 +158,15 @@ class ReportService:
|
||||
query_db: AsyncSession,
|
||||
query_object: ConsumptionReportQueryModel,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> PageModel[ConsumptionReportModel]:
|
||||
site_ids = await cls._site_scope(query_db, current_user)
|
||||
site_ids = await cls._site_scope(query_db, current_user, test_scope)
|
||||
if site_ids == []:
|
||||
return cls._empty_page(query_object)
|
||||
page = await ReportDao.get_consumption_report_list(
|
||||
query_db,
|
||||
query_object,
|
||||
test_scope,
|
||||
site_ids=site_ids,
|
||||
)
|
||||
return cls._convert_page(page, ConsumptionReportModel)
|
||||
@@ -165,13 +177,15 @@ class ReportService:
|
||||
query_db: AsyncSession,
|
||||
query_object: ReturnReportQueryModel,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> PageModel[ReturnReportModel]:
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user, test_scope)
|
||||
if warehouse_ids == [] and site_ids == []:
|
||||
return cls._empty_page(query_object)
|
||||
page = await ReportDao.get_return_report_list(
|
||||
query_db,
|
||||
query_object,
|
||||
test_scope,
|
||||
site_ids=site_ids,
|
||||
warehouse_ids=warehouse_ids,
|
||||
)
|
||||
@@ -183,13 +197,15 @@ class ReportService:
|
||||
query_db: AsyncSession,
|
||||
query_object: PalletTraceReportQueryModel,
|
||||
current_user: CurrentUserModel,
|
||||
test_scope: TestBatchScope,
|
||||
) -> PageModel[PalletTraceReportModel]:
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user)
|
||||
warehouse_ids, site_ids = await cls._scopes(query_db, current_user, test_scope)
|
||||
if warehouse_ids == [] and site_ids == []:
|
||||
return cls._empty_page(query_object)
|
||||
page = await ReportDao.get_pallet_trace_list(
|
||||
query_db,
|
||||
query_object,
|
||||
test_scope,
|
||||
warehouse_ids=warehouse_ids,
|
||||
site_ids=site_ids,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ from sqlalchemy.sql.elements import BinaryExpression, BindParameter, BooleanClau
|
||||
|
||||
from exceptions.exception import PermissionException
|
||||
from module_business.dao.access_dao import AccessDao
|
||||
from module_business.domain.test_batch_scope import TestBatchScope
|
||||
from module_business.entity.do.master_do import BizUserSite, BizUserWarehouse
|
||||
from module_business.service.access_service import AccessService
|
||||
|
||||
@@ -260,32 +261,28 @@ class AccessDaoTest(unittest.IsolatedAsyncioTestCase):
|
||||
result.scalars.return_value.all.return_value = (11, 13)
|
||||
db = self._db_with_result(result)
|
||||
|
||||
warehouse_ids = await AccessDao.list_user_warehouse_ids(db, user_id=8)
|
||||
warehouse_ids = await AccessDao.list_user_warehouse_ids(db, user_id=8, scope=TestBatchScope.formal())
|
||||
|
||||
self.assertEqual([11, 13], warehouse_ids)
|
||||
statement = db.execute.await_args.args[0]
|
||||
self._assert_statement_targets(
|
||||
statement,
|
||||
BizUserWarehouse,
|
||||
BizUserWarehouse.warehouse_id,
|
||||
((BizUserWarehouse.user_id, 8),),
|
||||
)
|
||||
sql = ' '.join(str(statement.compile(compile_kwargs={'literal_binds': True})).split())
|
||||
self.assertIn('JOIN biz_warehouse', sql)
|
||||
self.assertIn('biz_user_warehouse.user_id = 8', sql)
|
||||
self.assertIn('biz_warehouse.test_batch_id IS NULL', sql)
|
||||
|
||||
async def test_list_user_site_ids_returns_scalar_ids(self) -> None:
|
||||
result = MagicMock()
|
||||
result.scalars.return_value.all.return_value = (3, 5)
|
||||
db = self._db_with_result(result)
|
||||
|
||||
site_ids = await AccessDao.list_user_site_ids(db, user_id=8)
|
||||
site_ids = await AccessDao.list_user_site_ids(db, user_id=8, scope=TestBatchScope.formal())
|
||||
|
||||
self.assertEqual([3, 5], site_ids)
|
||||
statement = db.execute.await_args.args[0]
|
||||
self._assert_statement_targets(
|
||||
statement,
|
||||
BizUserSite,
|
||||
BizUserSite.site_id,
|
||||
((BizUserSite.user_id, 8),),
|
||||
)
|
||||
sql = ' '.join(str(statement.compile(compile_kwargs={'literal_binds': True})).split())
|
||||
self.assertIn('JOIN biz_customer_site', sql)
|
||||
self.assertIn('biz_user_site.user_id = 8', sql)
|
||||
self.assertIn('biz_customer_site.test_batch_id IS NULL', sql)
|
||||
|
||||
async def test_replace_warehouse_users_replaces_complete_mapping(self) -> None:
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
|
||||
@@ -22,6 +22,7 @@ from module_business.controller import report_controller as report_controller_mo
|
||||
from module_business.dao import report_dao as report_dao_module
|
||||
from module_business.dao.access_dao import AccessDao
|
||||
from module_business.domain.enums import DocumentStatus, PalletEventType, PalletFlowState, ReturnStatus
|
||||
from module_business.domain.test_batch_scope import TestBatchScope
|
||||
from module_business.entity.vo.report_vo import (
|
||||
ConsumptionReportModel,
|
||||
ConsumptionReportQueryModel,
|
||||
@@ -206,12 +207,12 @@ class TestReportPublicContract(unittest.TestCase):
|
||||
report_dao = getattr(report_dao_module, 'ReportDao', None)
|
||||
self.assertIsNotNone(report_dao, 'ReportDao 尚未实现')
|
||||
expected_signatures = {
|
||||
'get_pallet_inventory_list': ('db', 'query_object', 'warehouse_ids', 'site_ids'),
|
||||
'get_filling_report_list': ('db', 'query_object', 'warehouse_ids'),
|
||||
'get_shipment_report_list': ('db', 'query_object', 'warehouse_ids', 'site_ids'),
|
||||
'get_consumption_report_list': ('db', 'query_object', 'site_ids'),
|
||||
'get_return_report_list': ('db', 'query_object', 'site_ids', 'warehouse_ids'),
|
||||
'get_pallet_trace_list': ('db', 'query_object', 'warehouse_ids', 'site_ids'),
|
||||
'get_pallet_inventory_list': ('db', 'query_object', 'scope', 'warehouse_ids', 'site_ids'),
|
||||
'get_filling_report_list': ('db', 'query_object', 'scope', 'warehouse_ids'),
|
||||
'get_shipment_report_list': ('db', 'query_object', 'scope', 'warehouse_ids', 'site_ids'),
|
||||
'get_consumption_report_list': ('db', 'query_object', 'scope', 'site_ids'),
|
||||
'get_return_report_list': ('db', 'query_object', 'scope', 'site_ids', 'warehouse_ids'),
|
||||
'get_pallet_trace_list': ('db', 'query_object', 'scope', 'warehouse_ids', 'site_ids'),
|
||||
}
|
||||
|
||||
for method_name, expected_parameters in expected_signatures.items():
|
||||
@@ -237,7 +238,7 @@ class TestReportPublicContract(unittest.TestCase):
|
||||
method = getattr(report_service, method_name, None)
|
||||
self.assertIsNotNone(method, f'ReportService.{method_name} 尚未实现')
|
||||
self.assertEqual(
|
||||
('query_db', 'query_object', 'current_user'),
|
||||
('query_db', 'query_object', 'current_user', 'test_scope'),
|
||||
tuple(inspect.signature(method).parameters),
|
||||
)
|
||||
|
||||
@@ -335,6 +336,7 @@ class TestReportDaoQueries(unittest.IsolatedAsyncioTestCase):
|
||||
hasNext=False,
|
||||
)
|
||||
paginate = AsyncMock(return_value=page)
|
||||
scope.setdefault('scope', TestBatchScope.formal())
|
||||
with patch.object(PageUtil, 'paginate', new=paginate):
|
||||
try:
|
||||
result = await method(self.db, query_object, **scope)
|
||||
@@ -659,23 +661,24 @@ class TestReportServices(unittest.IsolatedAsyncioTestCase):
|
||||
dao_method = AsyncMock(return_value=page)
|
||||
warehouse_scope = AsyncMock(return_value=[8, 7, 7])
|
||||
site_scope = AsyncMock(return_value=[10, 9, 9])
|
||||
test_scope = TestBatchScope.formal()
|
||||
with (
|
||||
patch.object(ReportDao, dao_name, new=dao_method),
|
||||
patch.object(AccessDao, 'list_user_warehouse_ids', new=warehouse_scope),
|
||||
patch.object(AccessDao, 'list_user_site_ids', new=site_scope),
|
||||
):
|
||||
try:
|
||||
result = await getattr(ReportService, service_name)(self.db, query_object, self.user)
|
||||
result = await getattr(ReportService, service_name)(self.db, query_object, self.user, test_scope)
|
||||
except NotImplementedError:
|
||||
self.fail(f'ReportService.{service_name} 尚未实现数据范围和行转换')
|
||||
|
||||
dao_method.assert_awaited_once_with(self.db, query_object, **expected_scope)
|
||||
dao_method.assert_awaited_once_with(self.db, query_object, test_scope, **expected_scope)
|
||||
if 'warehouse_ids' in expected_scope:
|
||||
warehouse_scope.assert_awaited_once_with(self.db, 7)
|
||||
warehouse_scope.assert_awaited_once_with(self.db, 7, test_scope)
|
||||
else:
|
||||
warehouse_scope.assert_not_awaited()
|
||||
if 'site_ids' in expected_scope:
|
||||
site_scope.assert_awaited_once_with(self.db, 7)
|
||||
site_scope.assert_awaited_once_with(self.db, 7, test_scope)
|
||||
else:
|
||||
site_scope.assert_not_awaited()
|
||||
self.assertEqual(17, result.total)
|
||||
@@ -779,6 +782,7 @@ class TestReportServices(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
for service_name, dao_name, query_object, expected_scope in cases:
|
||||
with self.subTest(service_name=service_name):
|
||||
test_scope = TestBatchScope.formal()
|
||||
dao_method = AsyncMock(
|
||||
return_value=PageModel(
|
||||
rows=[],
|
||||
@@ -796,13 +800,13 @@ class TestReportServices(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(AccessDao, 'list_user_site_ids', new=site_scope),
|
||||
):
|
||||
try:
|
||||
await getattr(ReportService, service_name)(self.db, query_object, make_user(1))
|
||||
await getattr(ReportService, service_name)(self.db, query_object, make_user(1), test_scope)
|
||||
except NotImplementedError:
|
||||
self.fail(f'ReportService.{service_name} 尚未实现管理员范围语义')
|
||||
|
||||
warehouse_scope.assert_not_awaited()
|
||||
site_scope.assert_not_awaited()
|
||||
dao_method.assert_awaited_once_with(self.db, query_object, **expected_scope)
|
||||
dao_method.assert_awaited_once_with(self.db, query_object, test_scope, **expected_scope)
|
||||
|
||||
async def _assert_empty_page_without_dao(
|
||||
self,
|
||||
@@ -813,6 +817,7 @@ class TestReportServices(unittest.IsolatedAsyncioTestCase):
|
||||
site_ids: list[int] | None,
|
||||
) -> None:
|
||||
dao_method = AsyncMock()
|
||||
test_scope = TestBatchScope.formal()
|
||||
with (
|
||||
patch.object(ReportDao, dao_name, new=dao_method),
|
||||
patch.object(
|
||||
@@ -827,7 +832,7 @@ class TestReportServices(unittest.IsolatedAsyncioTestCase):
|
||||
),
|
||||
):
|
||||
try:
|
||||
result = await getattr(ReportService, service_name)(self.db, query_object, self.user)
|
||||
result = await getattr(ReportService, service_name)(self.db, query_object, self.user, test_scope)
|
||||
except NotImplementedError:
|
||||
self.fail(f'ReportService.{service_name} 尚未实现空范围短路')
|
||||
|
||||
@@ -888,7 +893,12 @@ class TestReportServices(unittest.IsolatedAsyncioTestCase):
|
||||
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[])),
|
||||
):
|
||||
try:
|
||||
await getattr(ReportService, service_name)(self.db, query_object, self.user)
|
||||
await getattr(ReportService, service_name)(
|
||||
self.db,
|
||||
query_object,
|
||||
self.user,
|
||||
TestBatchScope.formal(),
|
||||
)
|
||||
except NotImplementedError:
|
||||
self.fail(f'ReportService.{service_name} 尚未实现单侧 OR 范围可见性')
|
||||
dao_method.assert_awaited_once()
|
||||
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
import ast
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from sqlalchemy.dialects import mysql, postgresql
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.vo import PageModel
|
||||
from module_business.dao import report_dao as report_dao_module
|
||||
from module_business.dao.access_dao import AccessDao
|
||||
from module_business.domain.test_batch_scope import TestBatchScope
|
||||
from module_business.entity.vo.report_vo import (
|
||||
ConsumptionReportQueryModel,
|
||||
FillingReportQueryModel,
|
||||
PalletInventoryReportQueryModel,
|
||||
PalletTraceReportQueryModel,
|
||||
ReturnReportQueryModel,
|
||||
ShipmentReportQueryModel,
|
||||
)
|
||||
from module_business.service.report_service import ReportService
|
||||
from utils.page_util import PageUtil
|
||||
|
||||
BACKEND_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
SCOPED_DAO_METHODS = {
|
||||
'module_business/dao/master_dao.py': {
|
||||
'list_strain',
|
||||
'get_strain',
|
||||
'list_customer',
|
||||
'get_customer',
|
||||
'list_site',
|
||||
'get_site',
|
||||
'list_line',
|
||||
'get_line',
|
||||
'list_warehouse',
|
||||
'get_warehouse',
|
||||
'list_pallet',
|
||||
'get_pallet',
|
||||
},
|
||||
'module_business/dao/operation_dao.py': {
|
||||
'get_inbound_list',
|
||||
'get_inbound_by_id',
|
||||
'get_filling_list',
|
||||
'get_filling_by_id',
|
||||
'get_shipment_list',
|
||||
'get_shipment_by_id',
|
||||
'get_usage_list',
|
||||
'get_usage_by_id',
|
||||
'get_return_list',
|
||||
'get_return_by_id',
|
||||
},
|
||||
'module_business/dao/report_dao.py': {
|
||||
'get_pallet_inventory_list',
|
||||
'get_filling_report_list',
|
||||
'get_shipment_report_list',
|
||||
'get_consumption_report_list',
|
||||
'get_return_report_list',
|
||||
'get_pallet_trace_list',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def make_test_scope(batch_id: int) -> TestBatchScope:
|
||||
return TestBatchScope(
|
||||
batch_id=batch_id,
|
||||
mode='TEST',
|
||||
status=SimpleNamespace(value='ACTIVE'),
|
||||
writable=True,
|
||||
)
|
||||
|
||||
|
||||
def _compile(statement: object) -> str:
|
||||
return ' '.join(
|
||||
' '.join(str(statement.compile(dialect=dialect, compile_kwargs={'literal_binds': True})).lower().split())
|
||||
for dialect in (mysql.dialect(), postgresql.dialect())
|
||||
)
|
||||
|
||||
|
||||
class TestScopedDaoSignatureContract(unittest.TestCase):
|
||||
def test_all_public_read_daos_require_test_batch_scope(self) -> None:
|
||||
for relative_path, method_names in SCOPED_DAO_METHODS.items():
|
||||
tree = ast.parse((BACKEND_ROOT / relative_path).read_text(encoding='utf-8'))
|
||||
methods = {
|
||||
node.name: node for node in ast.walk(tree) if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef))
|
||||
}
|
||||
for method_name in method_names:
|
||||
with self.subTest(path=relative_path, method=method_name):
|
||||
method = methods[method_name]
|
||||
parameters = [*method.args.posonlyargs, *method.args.args, *method.args.kwonlyargs]
|
||||
scope = next((parameter for parameter in parameters if parameter.arg == 'scope'), None)
|
||||
self.assertIsNotNone(scope)
|
||||
self.assertIn('TestBatchScope', ast.unparse(scope.annotation))
|
||||
|
||||
|
||||
class TestAccessDaoBatchScope(unittest.IsolatedAsyncioTestCase):
|
||||
async def _capture_ids(self, method_name: str, scope: TestBatchScope) -> str:
|
||||
result = MagicMock()
|
||||
result.scalars.return_value.all.return_value = [7]
|
||||
db = AsyncMock(spec=AsyncSession)
|
||||
db.execute.return_value = result
|
||||
|
||||
await getattr(AccessDao, method_name)(db, 8, scope)
|
||||
statement = db.execute.await_args.args[0]
|
||||
return _compile(statement)
|
||||
|
||||
async def test_authorized_warehouse_ids_join_and_filter_resource_scope(self) -> None:
|
||||
sql = await self._capture_ids('list_user_warehouse_ids', make_test_scope(7))
|
||||
self.assertIn('join biz_warehouse', sql)
|
||||
self.assertIn('biz_warehouse.test_batch_id = 7', sql)
|
||||
|
||||
async def test_authorized_site_ids_formal_scope_filters_formal_resource(self) -> None:
|
||||
sql = await self._capture_ids('list_user_site_ids', TestBatchScope.formal())
|
||||
self.assertIn('join biz_customer_site', sql)
|
||||
self.assertIn('biz_customer_site.test_batch_id is null', sql)
|
||||
|
||||
|
||||
class TestReportSqlBatchScope(unittest.IsolatedAsyncioTestCase):
|
||||
async def _capture_report_sql(self, method_name: str, query_object: object) -> str:
|
||||
db = MagicMock(spec=AsyncSession)
|
||||
page = PageModel(
|
||||
rows=[], pageNum=query_object.page_num, pageSize=query_object.page_size, total=0, hasNext=False
|
||||
)
|
||||
paginate = AsyncMock(return_value=page)
|
||||
scope_arguments = {
|
||||
'get_pallet_inventory_list': {'warehouse_ids': [8, 7], 'site_ids': [10, 9]},
|
||||
'get_filling_report_list': {'warehouse_ids': [8, 7]},
|
||||
'get_shipment_report_list': {'warehouse_ids': [8, 7], 'site_ids': [10, 9]},
|
||||
'get_consumption_report_list': {'site_ids': [10, 9]},
|
||||
'get_return_report_list': {'site_ids': [10, 9], 'warehouse_ids': [8, 7]},
|
||||
'get_pallet_trace_list': {'warehouse_ids': [8, 7], 'site_ids': [10, 9]},
|
||||
}
|
||||
with patch.object(PageUtil, 'paginate', new=paginate):
|
||||
result = await getattr(report_dao_module.ReportDao, method_name)(
|
||||
db,
|
||||
query_object,
|
||||
make_test_scope(7),
|
||||
**scope_arguments[method_name],
|
||||
)
|
||||
self.assertIs(page, result)
|
||||
return _compile(paginate.await_args.args[1])
|
||||
|
||||
async def test_each_report_root_query_contains_test_batch_condition(self) -> None:
|
||||
cases = (
|
||||
(
|
||||
'get_pallet_inventory_list',
|
||||
PalletInventoryReportQueryModel(),
|
||||
(
|
||||
'biz_pallet_inventory.test_batch_id = 7',
|
||||
'biz_pallet.test_batch_id = 7',
|
||||
'active_filling.test_batch_id = 7',
|
||||
),
|
||||
),
|
||||
(
|
||||
'get_filling_report_list',
|
||||
FillingReportQueryModel(),
|
||||
(
|
||||
'biz_filling_record.test_batch_id = 7',
|
||||
'biz_pallet.test_batch_id = 7',
|
||||
'biz_warehouse.test_batch_id = 7',
|
||||
),
|
||||
),
|
||||
(
|
||||
'get_shipment_report_list',
|
||||
ShipmentReportQueryModel(),
|
||||
(
|
||||
'biz_shipment_order.test_batch_id = 7',
|
||||
'biz_shipment_item.test_batch_id = 7',
|
||||
'biz_pallet.test_batch_id = 7',
|
||||
'source_warehouse.test_batch_id = 7',
|
||||
'target_site.test_batch_id = 7',
|
||||
'target_customer.test_batch_id = 7',
|
||||
),
|
||||
),
|
||||
(
|
||||
'get_consumption_report_list',
|
||||
ConsumptionReportQueryModel(),
|
||||
(
|
||||
'biz_usage_record.test_batch_id = 7',
|
||||
'biz_pallet.test_batch_id = 7',
|
||||
'biz_filling_record.test_batch_id = 7',
|
||||
'biz_customer_site.test_batch_id = 7',
|
||||
'biz_production_line.test_batch_id = 7',
|
||||
'biz_customer.test_batch_id = 7',
|
||||
),
|
||||
),
|
||||
(
|
||||
'get_return_report_list',
|
||||
ReturnReportQueryModel(),
|
||||
(
|
||||
'biz_return_order.test_batch_id = 7',
|
||||
'biz_return_item.test_batch_id = 7',
|
||||
'biz_pallet.test_batch_id = 7',
|
||||
'source_site.test_batch_id = 7',
|
||||
'target_warehouse.test_batch_id = 7',
|
||||
),
|
||||
),
|
||||
(
|
||||
'get_pallet_trace_list',
|
||||
PalletTraceReportQueryModel(),
|
||||
(
|
||||
'biz_pallet_event.test_batch_id = 7',
|
||||
'biz_pallet.test_batch_id = 7',
|
||||
'before_warehouse.test_batch_id = 7',
|
||||
'after_warehouse.test_batch_id = 7',
|
||||
'before_site.test_batch_id = 7',
|
||||
'after_site.test_batch_id = 7',
|
||||
'before_line.test_batch_id = 7',
|
||||
'after_line.test_batch_id = 7',
|
||||
),
|
||||
),
|
||||
)
|
||||
for method_name, query_object, expected_fragments in cases:
|
||||
with self.subTest(method=method_name):
|
||||
sql = await self._capture_report_sql(method_name, query_object)
|
||||
for fragment in expected_fragments:
|
||||
self.assertIn(fragment, sql)
|
||||
|
||||
|
||||
class TestReportServiceScopeForwarding(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_formal_scope_is_forwarded_to_report_dao(self) -> None:
|
||||
db = MagicMock(spec=AsyncSession)
|
||||
user = SimpleNamespace(user=SimpleNamespace(user_id=1, admin=True))
|
||||
query = PalletInventoryReportQueryModel()
|
||||
page = PageModel(rows=[], pageNum=1, pageSize=10, total=0, hasNext=False)
|
||||
dao = AsyncMock(return_value=page)
|
||||
with patch.object(report_dao_module.ReportDao, 'get_pallet_inventory_list', new=dao):
|
||||
await ReportService.get_pallet_inventory_list_services(db, query, user, TestBatchScope.formal())
|
||||
self.assertEqual(TestBatchScope.formal(), dao.await_args.args[2])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user