更新登录页及优化相关功能
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
"""接收方按原发货单核对收货;不重新编托盘号、批号或装填记录。"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import NoReturn
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.vo import CrudResponseModel, PageModel
|
||||
from exceptions.exception import PermissionException, ServiceException
|
||||
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
|
||||
from module_business.dao.master_dao import MasterDao
|
||||
from module_business.dao.operation_dao import OperationDao
|
||||
from module_business.dao.shipment_receipt_dao import ShipmentReceiptDao
|
||||
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, assert_record_in_scope
|
||||
from module_business.entity.do.master_do import BizPallet
|
||||
from module_business.entity.do.operation_do import BizPalletEvent, BizPalletInventory, BizShipmentItem, BizShipmentOrder
|
||||
from module_business.entity.vo.operation_vo import ShipmentModel
|
||||
from module_business.entity.vo.shipment_receipt_vo import (
|
||||
ShipmentReceiptCommandModel,
|
||||
ShipmentReceiptItemModel,
|
||||
ShipmentReceiptModel,
|
||||
ShipmentReceiptQueryModel,
|
||||
)
|
||||
from module_business.service.access_service import AccessService
|
||||
from module_business.service.test_batch_access_service import TestBatchAccessService
|
||||
|
||||
|
||||
class ShipmentReceiptService:
|
||||
@staticmethod
|
||||
async def _site_scope(db: AsyncSession, user: CurrentUserModel) -> list[int] | None:
|
||||
if user.user is None or user.user.user_id is None:
|
||||
raise PermissionException(data='', message='当前用户信息无效')
|
||||
if user.user.admin:
|
||||
return None
|
||||
return sorted(set(await AccessDao.list_user_site_ids(db, user.user.user_id)))
|
||||
|
||||
@staticmethod
|
||||
def _missing() -> NoReturn:
|
||||
raise PermissionException(data='', message='无权访问该收货单或单据不存在')
|
||||
|
||||
@classmethod
|
||||
async def list_services(
|
||||
cls, db: AsyncSession, query: ShipmentReceiptQueryModel, user: CurrentUserModel, scope: TestBatchScope
|
||||
) -> PageModel:
|
||||
site_ids = await cls._site_scope(db, user)
|
||||
if query.target_site_id and site_ids is not None and query.target_site_id not in site_ids:
|
||||
cls._missing()
|
||||
result = await ShipmentReceiptDao.list_orders(db, query, scope, site_ids)
|
||||
return result.model_copy(update={'rows': [ShipmentModel.model_validate(row) for row in result.rows]})
|
||||
|
||||
@classmethod
|
||||
async def _order(
|
||||
cls, db: AsyncSession, shipment_id: int, user: CurrentUserModel, scope: TestBatchScope
|
||||
) -> BizShipmentOrder:
|
||||
site_ids = await cls._site_scope(db, user)
|
||||
order = await OperationDao.get_shipment_by_id(db, shipment_id, scope, warehouse_ids=None, site_ids=site_ids)
|
||||
if order is None:
|
||||
cls._missing()
|
||||
assert_record_in_scope(order, scope, '发货单')
|
||||
await AccessService.assert_site_access(db, user, order.target_site_id)
|
||||
if order.status != 'CONFIRMED':
|
||||
raise ServiceException(message='发货草稿尚未出库,不能核对收货')
|
||||
return order
|
||||
|
||||
@staticmethod
|
||||
async def _item_model(db: AsyncSession, item: BizShipmentItem, scope: TestBatchScope) -> ShipmentReceiptItemModel:
|
||||
assert_record_in_scope(item, scope, '发货明细')
|
||||
pallet = await MasterDao.get_pallet(db, item.pallet_id, scope)
|
||||
if pallet is None or item.active_filling_id is None or item.package_qty is None:
|
||||
raise ServiceException(message='发货物料快照不完整')
|
||||
assert_record_in_scope(pallet, scope, '托盘')
|
||||
filling = await OperationDao.get_filling_by_id(db, item.active_filling_id, scope)
|
||||
if filling is None:
|
||||
raise ServiceException(message='原发货装填记录不存在')
|
||||
assert_record_in_scope(filling, scope, '装填记录')
|
||||
if filling.pallet_id != item.pallet_id:
|
||||
raise ServiceException(message='原发货托盘与批次不一致')
|
||||
return ShipmentReceiptItemModel.model_validate(
|
||||
{
|
||||
'shipment_item_id': item.shipment_item_id,
|
||||
'shipment_id': item.shipment_id,
|
||||
'pallet_id': item.pallet_id,
|
||||
'active_filling_id': item.active_filling_id,
|
||||
'package_qty': item.package_qty,
|
||||
'pallet_code': pallet.pallet_code,
|
||||
'qr_token': pallet.qr_token,
|
||||
'strain_name': filling.strain_name_snapshot,
|
||||
'batch_no': filling.batch_no,
|
||||
'production_date': filling.production_date,
|
||||
'expiry_date': filling.expiry_date,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def detail_services(
|
||||
cls, db: AsyncSession, shipment_id: int, user: CurrentUserModel, scope: TestBatchScope
|
||||
) -> ShipmentReceiptModel:
|
||||
order = await cls._order(db, shipment_id, user, scope)
|
||||
data = ShipmentModel.model_validate(order).model_dump()
|
||||
data['items'] = [await cls._item_model(db, item, scope) for item in order.items]
|
||||
return ShipmentReceiptModel.model_validate(data)
|
||||
|
||||
@staticmethod
|
||||
def _validate_inventory(
|
||||
order: BizShipmentOrder,
|
||||
item: BizShipmentItem,
|
||||
inventory: BizPalletInventory,
|
||||
pallet: BizPallet,
|
||||
scope: TestBatchScope,
|
||||
) -> None:
|
||||
assert_record_in_scope(item, scope, '发货明细')
|
||||
assert_record_in_scope(inventory, scope, '库存')
|
||||
assert_record_in_scope(pallet, scope, '托盘')
|
||||
if (
|
||||
item.shipment_id != order.shipment_id
|
||||
or item.pallet_id != pallet.pallet_id
|
||||
or inventory.pallet_id != pallet.pallet_id
|
||||
or pallet.asset_status != '0'
|
||||
or inventory.flow_state != PalletFlowState.IN_TRANSIT.value
|
||||
or inventory.site_id != order.target_site_id
|
||||
or inventory.warehouse_id is not None
|
||||
or inventory.tunnel_id is not None
|
||||
or inventory.trace_no is not None
|
||||
or inventory.current_line_id is not None
|
||||
or inventory.active_filling_id != item.active_filling_id
|
||||
or item.active_filling_id is None
|
||||
or type(item.package_qty) is not int
|
||||
or item.package_qty <= 0
|
||||
or inventory.remaining_packages != item.package_qty
|
||||
or type(inventory.version) is not int
|
||||
or inventory.version < 0
|
||||
):
|
||||
raise ServiceException(message='待收货托盘状态、批次或包数与原发货单不一致')
|
||||
|
||||
@classmethod
|
||||
async def scan_services(
|
||||
cls, db: AsyncSession, shipment_id: int, identifier: str, user: CurrentUserModel, scope: TestBatchScope
|
||||
) -> ShipmentReceiptItemModel:
|
||||
order = await cls._order(db, shipment_id, user, scope)
|
||||
if order.receipt_state != 'PENDING':
|
||||
raise ServiceException(message='该发货单无需再次核对收货')
|
||||
pallet = await InventoryDao.get_pallet_by_qr_token(db, identifier, scope)
|
||||
if pallet is None:
|
||||
raise ServiceException(message='扫码托盘不属于该发货单')
|
||||
items = [item for item in order.items if item.pallet_id == pallet.pallet_id]
|
||||
if len(items) != 1:
|
||||
raise ServiceException(message='扫码托盘不属于该发货单')
|
||||
inventory = await MasterDao.get_pallet_inventory(db, pallet.pallet_id, scope)
|
||||
if inventory is None:
|
||||
raise ServiceException(message='托盘库存不存在')
|
||||
cls._validate_inventory(order, items[0], inventory, pallet, scope)
|
||||
return await cls._item_model(db, items[0], scope)
|
||||
|
||||
@staticmethod
|
||||
def _request_hash(shipment_id: int, command: ShipmentReceiptCommandModel) -> str:
|
||||
payload = {'shipment_id': shipment_id, 'scanned_pallet_ids': sorted(command.scanned_pallet_ids)}
|
||||
return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||
|
||||
@classmethod
|
||||
async def confirm_services( # noqa: PLR0912, PLR0915
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
shipment_id: int,
|
||||
command: ShipmentReceiptCommandModel,
|
||||
user: CurrentUserModel,
|
||||
scope: TestBatchScope,
|
||||
) -> CrudResponseModel:
|
||||
try:
|
||||
await TestBatchAccessService.lock_active_batch(db, scope)
|
||||
site_ids = await cls._site_scope(db, user)
|
||||
order = await OperationDao.get_shipment_for_update(
|
||||
db, shipment_id, scope, warehouse_ids=None, site_ids=site_ids
|
||||
)
|
||||
if order is None:
|
||||
cls._missing()
|
||||
assert_record_in_scope(order, scope, '发货单')
|
||||
# 已有发货查询权限不能替代收货方的目标现场权限;锁后再次核验。
|
||||
await AccessService.assert_site_access(db, user, order.target_site_id, for_update=True)
|
||||
request_hash = cls._request_hash(shipment_id, command)
|
||||
if order.status != 'CONFIRMED':
|
||||
raise ServiceException(message='仅已出库发货单可核对收货')
|
||||
if order.receipt_state == 'RECEIVED':
|
||||
if (
|
||||
order.receipt_idempotency_key != command.idempotency_key
|
||||
or order.receipt_request_hash != request_hash
|
||||
):
|
||||
raise ServiceException(message='发货单已收货,幂等信息不匹配')
|
||||
await db.rollback()
|
||||
return CrudResponseModel(is_success=True, message='收货核对成功', result=shipment_id)
|
||||
if order.receipt_state != 'PENDING':
|
||||
raise ServiceException(message='历史发货数据不重复确认收货')
|
||||
items = list(order.items)
|
||||
expected_ids = sorted(item.pallet_id for item in items)
|
||||
if (
|
||||
not expected_ids
|
||||
or len(expected_ids) != len(set(expected_ids))
|
||||
or expected_ids != sorted(command.scanned_pallet_ids)
|
||||
):
|
||||
raise ServiceException(message='核对托盘集合与原发货明细不一致')
|
||||
inventories = await InventoryDao.get_inventories_for_update(db, expected_ids, scope)
|
||||
pallets = await InventoryDao.get_pallets_by_ids(db, expected_ids, scope)
|
||||
if (
|
||||
sorted(i.pallet_id for i in inventories) != expected_ids
|
||||
or sorted(p.pallet_id for p in pallets) != expected_ids
|
||||
):
|
||||
raise ServiceException(message='待收货托盘或库存缺失')
|
||||
site = await MasterDao.get_site(db, order.target_site_id, scope, for_update=True)
|
||||
if site is None:
|
||||
raise ServiceException(message='收货现场不存在')
|
||||
assert_record_in_scope(site, scope, '收货现场')
|
||||
if site.status != '0':
|
||||
raise ServiceException(message='收货现场已停用')
|
||||
by_inventory = {i.pallet_id: i for i in inventories}
|
||||
by_pallet = {p.pallet_id: p for p in pallets}
|
||||
for item in items:
|
||||
cls._validate_inventory(order, item, by_inventory[item.pallet_id], by_pallet[item.pallet_id], scope)
|
||||
PalletStateMachine.ensure_transition(PalletFlowState.IN_TRANSIT, PalletFlowState.AT_SITE_UNUSED)
|
||||
|
||||
now = datetime.now()
|
||||
order.receipt_state = 'RECEIVED'
|
||||
order.received_at = now
|
||||
order.received_by = user.user.user_id
|
||||
order.receipt_idempotency_key = command.idempotency_key
|
||||
order.receipt_request_hash = request_hash
|
||||
order.update_by = user.user.user_name
|
||||
order.update_time = now
|
||||
for inventory in inventories:
|
||||
inventory.flow_state = PalletFlowState.AT_SITE_UNUSED.value
|
||||
inventory.site_arrived_at = now
|
||||
inventory.version += 1
|
||||
inventory.updated_at = now
|
||||
event = BizPalletEvent(
|
||||
pallet_id=inventory.pallet_id,
|
||||
event_type=PalletEventType.SHIPMENT_RECEIVED.value,
|
||||
source_type='SHIPMENT',
|
||||
source_id=shipment_id,
|
||||
before_state=PalletFlowState.IN_TRANSIT.value,
|
||||
after_state=inventory.flow_state,
|
||||
before_site_id=inventory.site_id,
|
||||
after_site_id=inventory.site_id,
|
||||
before_remaining_packages=inventory.remaining_packages,
|
||||
after_remaining_packages=inventory.remaining_packages,
|
||||
operator_id=user.user.user_id,
|
||||
occurred_at=now,
|
||||
idempotency_key=command.idempotency_key,
|
||||
request_hash=request_hash,
|
||||
test_batch_id=scope.batch_id,
|
||||
)
|
||||
await InventoryDao.add_event(db, inventory, event, scope)
|
||||
await db.commit()
|
||||
return CrudResponseModel(is_success=True, message='收货核对成功', result=shipment_id)
|
||||
except IntegrityError:
|
||||
await db.rollback()
|
||||
raise ServiceException(message='收货确认冲突,请刷新核对单后重试') from None
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
raise
|
||||
Reference in New Issue
Block a user