diff --git a/inventory-backend/app/api/v1/outbound.py b/inventory-backend/app/api/v1/outbound.py index 3fd55a6..36b07a5 100644 --- a/inventory-backend/app/api/v1/outbound.py +++ b/inventory-backend/app/api/v1/outbound.py @@ -468,6 +468,45 @@ def approve_outbound_request(request_id): return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500 +# -------------------------------------------------------- +# 5.5 手动完结/作废审批单 +# POST /api/v1/outbound/request//close +# -------------------------------------------------------- +@outbound_bp.route('/request//close', methods=['POST']) +@jwt_required() +@permission_required('outbound_approval') +def close_outbound_request(request_id): + """ + 手动完结/作废已通过的审批单(状态 1-已通过 → 4-已完结) + + 适用场景:已通过但无法出库/作废的单据,库管手动清理, + 使其从"已审批通过"列表中消失。 + """ + try: + user_id, user_role = get_current_user_info() + if not user_id: + return jsonify({'code': 401, 'msg': '用户未登录'}), 401 + + success, message, approval = OutboundApprovalService.close_request( + request_id=request_id, + user_id=user_id, + user_role=user_role + ) + + if not success: + return jsonify({'code': 400, 'msg': message}), 400 + + return jsonify({ + 'code': 200, + 'msg': message, + 'data': approval.to_dict() if approval else None + }), 200 + + except Exception as e: + traceback.print_exc() + return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500 + + # -------------------------------------------------------- # 6. 获取审批单列表 # GET /api/v1/outbound/request diff --git a/inventory-backend/app/models/outbound.py b/inventory-backend/app/models/outbound.py index e747499..68db370 100644 --- a/inventory-backend/app/models/outbound.py +++ b/inventory-backend/app/models/outbound.py @@ -82,7 +82,7 @@ class OutboundApproval(db.Model): 'applicant_name': self._get_user_name(self.applicant_id), 'remark': self.remark, 'status': self.status, - 'status_text': ['待审批', '已通过', '已驳回', '已完成'][self.status] if self.status in [0, 1, 2, 3] else '未知', + 'status_text': ['待审批', '已通过', '已驳回', '已完成', '已完结'][self.status] if self.status in [0, 1, 2, 3, 4] else '未知', 'allowed_approvers': self.get_allowed_approvers(), 'actual_approver_id': self.actual_approver_id, 'approver_name': self._get_user_name(self.actual_approver_id) if self.actual_approver_id else None, diff --git a/inventory-backend/app/services/outbound_service.py b/inventory-backend/app/services/outbound_service.py index 5b336cd..667c8f3 100644 --- a/inventory-backend/app/services/outbound_service.py +++ b/inventory-backend/app/services/outbound_service.py @@ -994,6 +994,52 @@ class OutboundApprovalService: logger.error(f"[Email] 外层发送异常: {e}") @staticmethod + def close_request(request_id, user_id, user_role): + """ + 手动完结/作废审批单(状态 1-已通过 → 4-已完结) + + 适用场景:已通过但无法出库/作废的单据,库管手动清理, + 使其从"已审批通过"列表中消失。 + + Args: + request_id: 审批单ID + user_id: 操作人ID + user_role: 操作人角色 + + Returns: + (success: bool, message: str, approval: OutboundApproval or None) + """ + from app.models.outbound import OutboundApproval + + approval = OutboundApproval.query.get(request_id) + if not approval: + return False, "审批单不存在", None + + if approval.status != 1: + return False, f"仅「已通过」的审批单可完结 (当前状态: {approval.status})", None + + # 权限检查:超级管理员、审批人 或 拥有出库操作权限(库管/主管) + if not OutboundApprovalService.can_approve(approval, user_id, user_role): + # 放宽:拥有 outbound_create:operation 的用户(库管)也可完结 + from app.models.system import SysRolePermission + has_outbound_op = SysRolePermission.query.filter( + SysRolePermission.role_code == user_role, + SysRolePermission.target_code.in_(['outbound_create:operation', 'outbound_create:*']) + ).first() is not None + if not has_outbound_op: + return False, "您没有完结此单的权限", None + + try: + approval.status = 4 # 4-已完结(手动作废) + approval.actual_approver_id = user_id + approval.approved_at = None + db.session.commit() + return True, "审批单已完结", approval + + except Exception as e: + db.session.rollback() + return False, f"完结失败: {str(e)}", None + def get_request_list(page=1, per_page=10, applicant_id=None, status=None): """ 获取审批单列表 diff --git a/inventory-web/src/api/outbound.ts b/inventory-web/src/api/outbound.ts index 29149db..d70d86c 100644 --- a/inventory-web/src/api/outbound.ts +++ b/inventory-web/src/api/outbound.ts @@ -124,6 +124,17 @@ export function approveRequest(id: number, data: { action: 'approve' | 'reject'; }) } +/** + * 手动完结/作废已通过的出库申请单(状态 1-已通过 → 4-已完结) + * @param id 审批单ID + */ +export function closeRequest(id: number) { + return request({ + url: `/v1/outbound/request/${id}/close`, + method: 'post' + }) +} + /** * BOM 匹配库存(替代前端 while(true) 全量加载) * 根据 BOM 子件的 base_id 列表,服务端精确查询有库存的记录 diff --git a/inventory-web/src/views/outbound/create.vue b/inventory-web/src/views/outbound/create.vue index 274fd99..7c3d802 100644 --- a/inventory-web/src/views/outbound/create.vue +++ b/inventory-web/src/views/outbound/create.vue @@ -46,6 +46,12 @@

仅显示已通过(status=1)的审批单

+
+ + 强制完结此单 + + 作废后该单将从下拉列表移除,出库记录将无法关联此单 +
@@ -284,7 +290,7 @@ import { ref, reactive, nextTick, onUnmounted, onMounted, computed } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue' import QrScanner from '@/components/QrScanner/index.vue' -import { getStockByBarcode, submitOutbound, getOutboundList, getApprovalRequestList } from '@/api/outbound' +import { getStockByBarcode, submitOutbound, getOutboundList, getApprovalRequestList, closeRequest } from '@/api/outbound' import { uploadFile } from '@/api/common/upload' import { useUserStore } from '@/stores/user' @@ -303,6 +309,7 @@ const formRef = ref() const approvalRequests = ref([]) const selectedRequest = ref(null) const requestsLoading = ref(false) +const closingRequest = ref(false) // 签名相关 const showSignatureDialog = ref(false) @@ -380,6 +387,37 @@ const handleRequestChange = (val: number | null) => { signaturePreviewUrl.value = '' } +// ★ 强制完结当前选中的申请单(作废,状态 1-已通过 → 4-已完结) +const handleCloseRequest = async () => { + if (!selectedRequest.value) return ElMessage.warning('请先选择要完结的审批单') + const req = selectedRequest.value + + try { + await ElMessageBox.confirm( + `确定强制完结审批单【${req.request_no}】吗?\n\n` + + `此操作将使该单从「已通过」列表中移除,且不可恢复。\n` + + `若该单已部分出库,请确认无需再关联此单。`, + '⚠️ 强制完结确认', + { confirmButtonText: '确认完结', cancelButtonText: '取消', type: 'warning' } + ) + } catch (e) { + return // 用户取消 + } + + closingRequest.value = true + try { + await closeRequest(req.id) + ElMessage.success(`审批单 ${req.request_no} 已完结`) + // 完结后刷新列表并清空当前选择 + selectedRequest.value = null + await loadApprovalRequests() + } catch (err: any) { + ElMessage.error(err?.msg || err?.message || '完结失败') + } finally { + closingRequest.value = false + } +} + // ★ 按单出库模式:校验扫码是否在计划内 const validateAgainstPlan = (scannedName: string, scannedSpec: string, scannedQty: number): string | null => { const normalizedName = scannedName.trim()