feat(outbound): 审批单手动完结/作废功能
- 后端: outbound_service 新增 close_request 方法(状态 1-已通过 → 4-已完结) - 后端: 新增 POST /api/v1/outbound/request/<id>/close 接口 - 模型: status_text 数组增加「已完结」 - 前端: create.vue 审批单选择区新增「强制完结此单」按钮 完结后刷新列表,该单从已通过下拉中移除 - 权限: 超级管理员/审批人/拥有 outbound_create:operation 的用户可完结(库管可操作)
This commit is contained in:
@ -468,6 +468,45 @@ def approve_outbound_request(request_id):
|
|||||||
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# 5.5 手动完结/作废审批单
|
||||||
|
# POST /api/v1/outbound/request/<id>/close
|
||||||
|
# --------------------------------------------------------
|
||||||
|
@outbound_bp.route('/request/<int:request_id>/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. 获取审批单列表
|
# 6. 获取审批单列表
|
||||||
# GET /api/v1/outbound/request
|
# GET /api/v1/outbound/request
|
||||||
|
|||||||
@ -82,7 +82,7 @@ class OutboundApproval(db.Model):
|
|||||||
'applicant_name': self._get_user_name(self.applicant_id),
|
'applicant_name': self._get_user_name(self.applicant_id),
|
||||||
'remark': self.remark,
|
'remark': self.remark,
|
||||||
'status': self.status,
|
'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(),
|
'allowed_approvers': self.get_allowed_approvers(),
|
||||||
'actual_approver_id': self.actual_approver_id,
|
'actual_approver_id': self.actual_approver_id,
|
||||||
'approver_name': self._get_user_name(self.actual_approver_id) if self.actual_approver_id else None,
|
'approver_name': self._get_user_name(self.actual_approver_id) if self.actual_approver_id else None,
|
||||||
|
|||||||
@ -994,6 +994,52 @@ class OutboundApprovalService:
|
|||||||
logger.error(f"[Email] 外层发送异常: {e}")
|
logger.error(f"[Email] 外层发送异常: {e}")
|
||||||
|
|
||||||
@staticmethod
|
@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):
|
def get_request_list(page=1, per_page=10, applicant_id=None, status=None):
|
||||||
"""
|
"""
|
||||||
获取审批单列表
|
获取审批单列表
|
||||||
|
|||||||
@ -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 匹配库存(替代前端 while(true) 全量加载)
|
||||||
* 根据 BOM 子件的 base_id 列表,服务端精确查询有库存的记录
|
* 根据 BOM 子件的 base_id 列表,服务端精确查询有库存的记录
|
||||||
|
|||||||
@ -46,6 +46,12 @@
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<p class="select-tip">仅显示已通过(status=1)的审批单</p>
|
<p class="select-tip">仅显示已通过(status=1)的审批单</p>
|
||||||
|
<div v-if="selectedRequest && userStore.hasPermission('outbound_create:operation')" style="margin-top: 8px; display: flex; align-items: center; gap: 8px;">
|
||||||
|
<el-button type="danger" plain size="small" :loading="closingRequest" @click="handleCloseRequest">
|
||||||
|
强制完结此单
|
||||||
|
</el-button>
|
||||||
|
<span style="color: #F56C6C; font-size: 12px;">作废后该单将从下拉列表移除,出库记录将无法关联此单</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ★ 按单出库:计划清单预览 -->
|
<!-- ★ 按单出库:计划清单预览 -->
|
||||||
@ -284,7 +290,7 @@ import { ref, reactive, nextTick, onUnmounted, onMounted, computed } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
||||||
import QrScanner from '@/components/QrScanner/index.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 { uploadFile } from '@/api/common/upload'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
@ -303,6 +309,7 @@ const formRef = ref()
|
|||||||
const approvalRequests = ref<any[]>([])
|
const approvalRequests = ref<any[]>([])
|
||||||
const selectedRequest = ref<any>(null)
|
const selectedRequest = ref<any>(null)
|
||||||
const requestsLoading = ref(false)
|
const requestsLoading = ref(false)
|
||||||
|
const closingRequest = ref(false)
|
||||||
|
|
||||||
// 签名相关
|
// 签名相关
|
||||||
const showSignatureDialog = ref(false)
|
const showSignatureDialog = ref(false)
|
||||||
@ -380,6 +387,37 @@ const handleRequestChange = (val: number | null) => {
|
|||||||
signaturePreviewUrl.value = ''
|
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 validateAgainstPlan = (scannedName: string, scannedSpec: string, scannedQty: number): string | null => {
|
||||||
const normalizedName = scannedName.trim()
|
const normalizedName = scannedName.trim()
|
||||||
|
|||||||
Reference in New Issue
Block a user