修复:收紧托盘业务权限与数据契约
This commit is contained in:
@@ -370,12 +370,14 @@ class OperationDao:
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
idempotency_key: str,
|
||||
warehouse_ids: list[int] | None = None,
|
||||
) -> BizInboundOrder | None:
|
||||
return await cls._get_by_key(
|
||||
db,
|
||||
BizInboundOrder,
|
||||
BizInboundOrder.confirm_idempotency_key,
|
||||
idempotency_key,
|
||||
*cls._inbound_scope_conditions(warehouse_ids),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import ColumnElement, Select, false, or_, select
|
||||
from sqlalchemy import ColumnElement, Select, and_, false, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import aliased
|
||||
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
|
||||
from module_business.domain.enums import DocumentStatus, PalletEventType
|
||||
from module_business.entity.do.master_do import (
|
||||
BizCustomer,
|
||||
BizCustomerSite,
|
||||
@@ -86,6 +86,41 @@ class ReportDao:
|
||||
]
|
||||
return [or_(*conditions)] if conditions else []
|
||||
|
||||
@classmethod
|
||||
def _pallet_trace_scope_conditions(
|
||||
cls,
|
||||
warehouse_ids: list[int] | None,
|
||||
site_ids: list[int] | None,
|
||||
) -> list[ColumnElement[bool]]:
|
||||
participant_scopes = [
|
||||
condition
|
||||
for column, scope_ids in (
|
||||
(BizPalletEvent.before_warehouse_id, warehouse_ids),
|
||||
(BizPalletEvent.after_warehouse_id, warehouse_ids),
|
||||
(BizPalletEvent.before_site_id, site_ids),
|
||||
(BizPalletEvent.after_site_id, site_ids),
|
||||
)
|
||||
if (condition := cls._scope_condition(column, scope_ids)) is not None
|
||||
]
|
||||
if not participant_scopes:
|
||||
return []
|
||||
|
||||
shipment_scopes = [
|
||||
condition
|
||||
for column, scope_ids in (
|
||||
(BizPalletEvent.before_warehouse_id, warehouse_ids),
|
||||
(BizPalletEvent.after_site_id, site_ids),
|
||||
)
|
||||
if (condition := cls._scope_condition(column, scope_ids)) is not None
|
||||
]
|
||||
shipment_type = PalletEventType.SHIPMENT_CONFIRMED.value
|
||||
return [
|
||||
or_(
|
||||
and_(BizPalletEvent.event_type == shipment_type, *shipment_scopes),
|
||||
and_(BizPalletEvent.event_type != shipment_type, or_(*participant_scopes)),
|
||||
)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
async def _paginate(
|
||||
db: AsyncSession,
|
||||
@@ -433,12 +468,7 @@ 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._or_scope_conditions(
|
||||
(BizPalletEvent.before_warehouse_id, warehouse_ids),
|
||||
(BizPalletEvent.after_warehouse_id, warehouse_ids),
|
||||
(BizPalletEvent.before_site_id, site_ids),
|
||||
(BizPalletEvent.after_site_id, site_ids),
|
||||
)
|
||||
conditions = 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:
|
||||
|
||||
@@ -45,7 +45,7 @@ class StrainPageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
strain_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='菌种编码')
|
||||
strain_name: str | None = Field(
|
||||
default=None,
|
||||
@@ -104,7 +104,7 @@ class CustomerPageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
customer_code: str | None = Field(
|
||||
default=None,
|
||||
min_length=1,
|
||||
@@ -171,7 +171,7 @@ class SitePageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
customer_id: int | None = Field(default=None, gt=0, description='客户ID')
|
||||
site_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='现场编码')
|
||||
site_name: str | None = Field(
|
||||
@@ -232,7 +232,7 @@ class LinePageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
site_id: int | None = Field(default=None, gt=0, description='现场ID')
|
||||
line_code: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='产线编码')
|
||||
line_name: str | None = Field(
|
||||
@@ -293,7 +293,7 @@ class WarehousePageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
warehouse_code: str | None = Field(
|
||||
default=None,
|
||||
min_length=1,
|
||||
|
||||
@@ -366,7 +366,7 @@ class InboundPageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
inbound_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='入库单号')
|
||||
warehouse_id: int | None = Field(default=None, gt=0, description='仓库ID')
|
||||
status: DocumentStatus | None = Field(default=None, description='单据状态')
|
||||
@@ -382,7 +382,7 @@ class FillingPageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
filling_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='装填单号')
|
||||
warehouse_id: int | None = Field(default=None, gt=0, description='仓库ID')
|
||||
strain_id: int | None = Field(default=None, gt=0, description='菌种ID')
|
||||
@@ -400,7 +400,7 @@ class ShipmentPageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
shipment_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='发货单号')
|
||||
source_warehouse_id: int | None = Field(default=None, gt=0, description='来源仓库ID')
|
||||
target_site_id: int | None = Field(default=None, gt=0, description='目标现场ID')
|
||||
@@ -417,7 +417,7 @@ class UsagePageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
usage_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='使用记录号')
|
||||
site_id: int | None = Field(default=None, gt=0, description='现场ID')
|
||||
line_id: int | None = Field(default=None, gt=0, description='产线ID')
|
||||
@@ -434,7 +434,7 @@ class ReturnPageQueryModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True)
|
||||
|
||||
page_num: int = Field(default=1, gt=0, description='当前页码')
|
||||
page_size: int = Field(default=10, gt=0, description='每页记录数')
|
||||
page_size: int = Field(default=10, gt=0, le=100, description='每页记录数')
|
||||
return_no: str | None = Field(default=None, min_length=1, max_length=64, pattern=r'\S', description='退回单号')
|
||||
source_site_id: int | None = Field(default=None, gt=0, description='来源现场ID')
|
||||
target_warehouse_id: int | None = Field(default=None, gt=0, description='目标仓库ID')
|
||||
|
||||
@@ -104,6 +104,64 @@ class InboundService:
|
||||
raise ServiceException(message='目标仓库已停用')
|
||||
return warehouse
|
||||
|
||||
@classmethod
|
||||
async def _lock_and_authorize_warehouses(
|
||||
cls,
|
||||
query_db: AsyncSession,
|
||||
current_user: CurrentUserModel,
|
||||
*,
|
||||
warehouse_ids: set[int],
|
||||
hidden_warehouse_ids: set[int],
|
||||
require_active: bool,
|
||||
) -> None:
|
||||
"""锁定仓库及授权映射,并基于锁后最新状态重新校验写权限。"""
|
||||
warehouses: dict[int, Any] = {}
|
||||
permission_errors: dict[int, PermissionException] = {}
|
||||
for warehouse_id in sorted(warehouse_ids):
|
||||
warehouses[warehouse_id] = await MasterDao.get_warehouse(
|
||||
query_db,
|
||||
warehouse_id,
|
||||
for_update=True,
|
||||
)
|
||||
try:
|
||||
await AccessService.assert_warehouse_access(
|
||||
query_db,
|
||||
current_user,
|
||||
warehouse_id,
|
||||
for_update=True,
|
||||
)
|
||||
except PermissionException as exc:
|
||||
permission_errors[warehouse_id] = exc
|
||||
|
||||
if hidden_warehouse_ids & permission_errors.keys():
|
||||
cls._raise_inbound_not_accessible(current_user)
|
||||
if permission_errors:
|
||||
raise permission_errors[min(permission_errors)]
|
||||
|
||||
for warehouse_id in sorted(warehouses):
|
||||
warehouse = warehouses[warehouse_id]
|
||||
if warehouse is None:
|
||||
raise ServiceException(message='目标仓库不存在')
|
||||
if require_active and warehouse.status != '0':
|
||||
raise ServiceException(message='目标仓库已停用')
|
||||
|
||||
@classmethod
|
||||
async def _lock_and_reauthorize_warehouse(
|
||||
cls,
|
||||
query_db: AsyncSession,
|
||||
inbound: BizInboundOrder,
|
||||
current_user: CurrentUserModel,
|
||||
*,
|
||||
require_active: bool = True,
|
||||
) -> None:
|
||||
await cls._lock_and_authorize_warehouses(
|
||||
query_db,
|
||||
current_user,
|
||||
warehouse_ids={inbound.warehouse_id},
|
||||
hidden_warehouse_ids={inbound.warehouse_id},
|
||||
require_active=require_active,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_pallet_pair(pallet: BizPallet, inventory: BizPalletInventory) -> None:
|
||||
if pallet.asset_status != '0':
|
||||
@@ -296,6 +354,13 @@ class InboundService:
|
||||
)
|
||||
ordered_ids = sorted(page_object.pallet_ids)
|
||||
await cls._lock_and_validate_pallets(query_db, ordered_ids)
|
||||
await cls._lock_and_authorize_warehouses(
|
||||
query_db,
|
||||
current_user,
|
||||
warehouse_ids={page_object.warehouse_id},
|
||||
hidden_warehouse_ids=set(),
|
||||
require_active=True,
|
||||
)
|
||||
|
||||
current_time = datetime.now()
|
||||
inbound = BizInboundOrder(
|
||||
@@ -348,6 +413,13 @@ class InboundService:
|
||||
)
|
||||
ordered_ids = sorted(page_object.pallet_ids)
|
||||
await cls._lock_and_validate_pallets(query_db, ordered_ids)
|
||||
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,
|
||||
)
|
||||
|
||||
current_time = datetime.now()
|
||||
inbound.warehouse_id = page_object.warehouse_id
|
||||
@@ -383,6 +455,7 @@ class InboundService:
|
||||
|
||||
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,
|
||||
@@ -393,6 +466,16 @@ class InboundService:
|
||||
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)
|
||||
@@ -429,17 +512,14 @@ class InboundService:
|
||||
current_user: CurrentUserModel,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
warehouse_ids = await cls._warehouse_scope(query_db, current_user)
|
||||
recovered = await OperationDao.get_inbound_by_confirm_idempotency_key(
|
||||
query_db,
|
||||
idempotency_key,
|
||||
warehouse_ids=warehouse_ids,
|
||||
)
|
||||
if recovered is None:
|
||||
raise ServiceException(message='确认入库失败,幂等冲突')
|
||||
await AccessService.assert_warehouse_access(
|
||||
query_db,
|
||||
current_user,
|
||||
recovered.warehouse_id,
|
||||
)
|
||||
if not cls._is_exact_confirmed(
|
||||
recovered,
|
||||
inbound_id,
|
||||
@@ -447,6 +527,26 @@ class InboundService:
|
||||
request_hash,
|
||||
):
|
||||
raise ServiceException(message='确认入库失败,幂等键已被其他请求使用')
|
||||
locked_recovered = await OperationDao.get_inbound_for_update(
|
||||
query_db,
|
||||
inbound_id,
|
||||
warehouse_ids=warehouse_ids,
|
||||
)
|
||||
if locked_recovered is None:
|
||||
cls._raise_inbound_not_accessible(current_user)
|
||||
if not cls._is_exact_confirmed(
|
||||
locked_recovered,
|
||||
inbound_id,
|
||||
idempotency_key,
|
||||
request_hash,
|
||||
):
|
||||
raise ServiceException(message='确认入库失败,幂等键已被其他请求使用')
|
||||
await cls._lock_and_reauthorize_warehouse(
|
||||
query_db,
|
||||
locked_recovered,
|
||||
current_user,
|
||||
require_active=False,
|
||||
)
|
||||
return cls._success('确认成功')
|
||||
finally:
|
||||
await query_db.rollback()
|
||||
@@ -475,6 +575,7 @@ class InboundService:
|
||||
existing = await OperationDao.get_inbound_by_confirm_idempotency_key(
|
||||
query_db,
|
||||
idempotency_key,
|
||||
warehouse_ids=warehouse_ids,
|
||||
)
|
||||
if existing is not None:
|
||||
await AccessService.assert_warehouse_access(
|
||||
@@ -504,6 +605,12 @@ class InboundService:
|
||||
idempotency_key,
|
||||
request_hash,
|
||||
):
|
||||
await cls._lock_and_reauthorize_warehouse(
|
||||
query_db,
|
||||
locked_inbound,
|
||||
current_user,
|
||||
require_active=False,
|
||||
)
|
||||
await query_db.rollback()
|
||||
return cls._success('确认成功')
|
||||
raise ServiceException(message='入库单已确认,幂等信息不匹配')
|
||||
@@ -521,6 +628,11 @@ class InboundService:
|
||||
raise ServiceException(message='入库单托盘不能重复')
|
||||
ordered_ids = sorted(pallet_ids)
|
||||
inventories, _ = await cls._lock_and_validate_pallets(query_db, ordered_ids)
|
||||
await cls._lock_and_reauthorize_warehouse(
|
||||
query_db,
|
||||
locked_inbound,
|
||||
current_user,
|
||||
)
|
||||
|
||||
for inventory in inventories:
|
||||
PalletStateMachine.ensure_transition(
|
||||
|
||||
@@ -1026,7 +1026,7 @@ class ReturnService:
|
||||
if scanned_ids != sorted(item_ids):
|
||||
raise ServiceException(message='确认扫码托盘集合与退回单明细不一致')
|
||||
|
||||
inventories, filling_by_id = await cls._lock_confirm_pallets(query_db, locked, items)
|
||||
inventories, _ = await cls._lock_confirm_pallets(query_db, locked, items)
|
||||
await cls._lock_and_reauthorize_confirm_warehouse(
|
||||
query_db,
|
||||
locked,
|
||||
@@ -1059,11 +1059,6 @@ class ReturnService:
|
||||
before_remaining_packages = inventory.remaining_packages
|
||||
if inventory.remaining_packages > 0:
|
||||
target_state = PalletFlowState.FILLED_IN_WAREHOUSE.value
|
||||
filling = filling_by_id[inventory.active_filling_id]
|
||||
filling.warehouse_id = locked.target_warehouse_id
|
||||
filling.update_by = user_name
|
||||
filling.update_time = current_time
|
||||
await OperationDao.update_filling_record(query_db, filling)
|
||||
else:
|
||||
target_state = PalletFlowState.EMPTY_IN_WAREHOUSE.value
|
||||
inventory.active_filling_id = None
|
||||
|
||||
@@ -148,7 +148,6 @@ class ShipmentService:
|
||||
if (
|
||||
filling.filling_id != inventory.active_filling_id
|
||||
or filling.pallet_id != pallet.pallet_id
|
||||
or filling.warehouse_id != source_warehouse_id
|
||||
or filling.active_slot != 'ACTIVE'
|
||||
):
|
||||
raise ServiceException(message='托盘活动装填周期无效')
|
||||
@@ -378,7 +377,6 @@ class ShipmentService:
|
||||
if (
|
||||
filling.filling_id != inventory.active_filling_id
|
||||
or filling.pallet_id != pallet.pallet_id
|
||||
or filling.warehouse_id != source_warehouse_id
|
||||
or filling.active_slot != 'ACTIVE'
|
||||
):
|
||||
raise ServiceException(message='托盘活动装填周期无效')
|
||||
@@ -424,11 +422,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,
|
||||
warehouse_ids=[query_object.source_warehouse_id],
|
||||
)
|
||||
filling = await OperationDao.get_filling_by_id(query_db, inventory.active_filling_id)
|
||||
if filling is None:
|
||||
raise ServiceException(message='托盘活动装填周期不存在')
|
||||
cls._validate_scan_pallet(
|
||||
|
||||
Reference in New Issue
Block a user