feat(scrap): 前端适配报废审批收口
配合后端把三条报废旁路收口到审批流。
【删除】api/scrap.ts 的 createScrap()
对应后端已删的 POST /api/v1/scrap。该函数此前已是死代码(全仓无调用方),
页面早已切换到申请-审批-按单执行流程。
【改造】不良品看板 views/stock/defective/index.vue
「报废销毁」→「申请报废」:弹窗新增「指定审批人」(必填,复用
getApproversList)。API 由 scrapDefective 改为 submitDefectiveScrapRequest。
★ 弹窗与成功提示都明确写出「在管数量不会立即扣减,待审批通过并执行后才
扣减」—— 申请不预占 remaining_qty,行数据看起来毫无变化,不说明会诱发
重复提交。
【改造】借还记录页 views/transaction/records.vue
「报废」→「申请报废」:弹窗新增「指定审批人」(必填)。原先内联的
request() 调用挪到 api/transaction.ts 成为 submitBorrowScrapRequest,
与全仓 API 分层一致。确认框文案改为「提交后进入审批流程」。
顺带清理 canScrap 里硬编码的 `username === 'IRIS'` 后门 —— 该账号在
sys_user 表中根本不存在,属死代码。
【改造】按单报废执行页 views/operation/scrap/create.vue
按 scrap_mode 把批准明细分流:
· scan 项(库存行 / 在管不良品)—— 走原有扫码购物车,逻辑不变
· auto 项(借出未还)—— 实物在借用人手上、无法扫码,放入**只读区块**
展示,不进购物车(购物车语义是「扫码证据」,混入会让扫码校验失效)
纯 auto 单隐藏整个扫码区并提示「本单无可扫码物料,将按批准数量直接执行」;
扫码进度只统计 scan 项(否则进度条永远满不了,会让操作员误以为没扫完);
提交守卫放宽为「购物车与 auto 项都为空才拦」;确认框追加
「另有 N 项将按批准数量自动执行」,避免操作员误以为只报废了扫到的那些。
matchKey 同步加入来源表(与后端 _match_key 保持逐字同口径),
sourceLabel 补两种新来源的中文名。
存量单据没有 scrap_mode 字段,前端回落为 scan,行为与改造前完全一致。
This commit is contained in:
@ -71,15 +71,26 @@ export function restockDefective(id: number, data: { restock_qty?: number; remar
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 报废销毁:在管坏件确认无法维修时销毁。
|
* 提交在管坏件的**报废申请**(需审批人审批,通过后由库管执行报废)。
|
||||||
|
*
|
||||||
|
* ★ 已由「直接报废」改为「申请」:
|
||||||
|
* 报废一律需审批(后端 SCRAP_ALWAYS_REQUIRES_APPROVAL)。原
|
||||||
|
* POST /defective/<id>/scrap 直接写 trans_scrap 并扣在管量,绕过审批,
|
||||||
|
* 构成职责分离漏洞(同一库管可自行宣告实物销毁而无人复核),已删除。
|
||||||
|
*
|
||||||
|
* ★ 副作用提醒:申请**不预占**在管量,`remaining_qty` 提交后不变。
|
||||||
|
* 前端必须提示用户「待审批并执行后才会扣减」,否则会诱发重复提交。
|
||||||
*
|
*
|
||||||
* @param id 在管台账 id
|
* @param id 在管台账 id
|
||||||
* @param data { scrap_qty?: number, reason: string }
|
* @param data { scrap_qty?: number, reason?: string, approver_id: number }
|
||||||
* reason 必填;scrap_qty 缺省 = 全部剩余在管量
|
* scrap_qty 缺省 = 全部剩余在管量;approver_id 必填
|
||||||
*/
|
*/
|
||||||
export function scrapDefective(id: number, data: { scrap_qty?: number; reason: string }) {
|
export function submitDefectiveScrapRequest(
|
||||||
|
id: number,
|
||||||
|
data: { scrap_qty?: number; reason?: string; approver_id: number },
|
||||||
|
) {
|
||||||
return request({
|
return request({
|
||||||
url: `/inbound/stock/defective/${id}/scrap`,
|
url: `/inbound/stock/defective/${id}/scrap-request`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,14 +9,11 @@ export function scanBarcode(barcode: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 提交报废单
|
// [已移除] 直接报废 createScrap —— 对应后端 POST /api/v1/scrap,该接口绕过
|
||||||
export function createScrap(data: any) {
|
// 审批流直接写 trans_scrap 并扣库存,与系统自陈的「报废一律需审批」规则冲突,
|
||||||
return request({
|
// 构成职责分离漏洞(同一库管可自行宣告实物销毁而无人复核),已整体删除。
|
||||||
url: '/v1/scrap',
|
// 所有报废改走:submitScrapRequest → approveScrapRequest → executeScrapByRequest。
|
||||||
method: 'post',
|
// 该函数此前已是死代码(全仓无调用方),删除无行为影响。
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 报废记录列表查询
|
// 3. 报废记录列表查询
|
||||||
export function getScrapRecords(params: any) {
|
export function getScrapRecords(params: any) {
|
||||||
|
|||||||
@ -230,4 +230,30 @@ export function dispatchBorrow(data: {
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交「借出未归还」的**报废申请**(需审批人审批,通过后由库管执行报废)。
|
||||||
|
*
|
||||||
|
* ★ 已由「直接报废」改为「申请」:
|
||||||
|
* 原 POST /v1/transactions/borrow/scrap 直接写 trans_scrap 并扣总库存,
|
||||||
|
* 绕过审批、构成职责分离漏洞,已删除。现统一走 申请 → 审批 → 执行。
|
||||||
|
*
|
||||||
|
* ★ 副作用提醒:申请**不立即改变**借用记录状态,提交后该笔仍显示为未归还。
|
||||||
|
* 前端必须提示用户「待审批并执行后才会标记为已报废」。
|
||||||
|
*
|
||||||
|
* @param data record_ids: trans_borrow.id 列表(按整条待还量报废)
|
||||||
|
* reason?: 写入申请单备注
|
||||||
|
* approver_id: 必填,指定审批人
|
||||||
|
*/
|
||||||
|
export function submitBorrowScrapRequest(data: {
|
||||||
|
record_ids: number[]
|
||||||
|
reason?: string
|
||||||
|
approver_id: number
|
||||||
|
}) {
|
||||||
|
return request({
|
||||||
|
url: '/v1/transactions/borrow/scrap-request',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@ -76,7 +76,43 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scan-section">
|
<!-- ★ 免扫码执行明细(借出未还):实物不在库、无法扫码,按批准量直接核销。
|
||||||
|
刻意**不进购物车** —— 购物车的语义是「扫码证据」,混入 auto 项会让
|
||||||
|
validateAgainstPlan / maxScrapFor 的扫码校验失去意义。 -->
|
||||||
|
<div v-if="selectedRequest && autoItems.length > 0" class="auto-items-section">
|
||||||
|
<el-alert type="info" :closable="false" show-icon>
|
||||||
|
以下明细的实物不在仓库内(借出未还),<b>无需扫码</b>,
|
||||||
|
执行时将按批准数量直接核销。
|
||||||
|
</el-alert>
|
||||||
|
<el-table :data="autoItems" border size="small" style="width: 100%; margin-top: 8px;">
|
||||||
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||||
|
<el-table-column label="来源" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" type="warning">{{ sourceLabel(row.source_table) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="sku" label="SKU" width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column label="执行数量" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span style="color: #E6A23C; font-weight: bold;">{{ row.scrap_qty ?? '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 纯 auto 单:没有可扫的实物,隐藏整个扫码区并明示执行方式 -->
|
||||||
|
<div v-if="selectedRequest && scanItems.length === 0" class="scan-section">
|
||||||
|
<el-alert
|
||||||
|
type="success"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="本单无可扫码物料"
|
||||||
|
description="所有明细均为免扫码来源,将按批准数量直接执行报废。"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="scan-section">
|
||||||
<!-- 扫码进度条 -->
|
<!-- 扫码进度条 -->
|
||||||
<div v-if="selectedRequest && scanTotalQty > 0" class="scan-progress">
|
<div v-if="selectedRequest && scanTotalQty > 0" class="scan-progress">
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
@ -295,28 +331,49 @@ const selectedRequestId = computed({
|
|||||||
|
|
||||||
const plannedItems = computed<any[]>(() => selectedRequest.value?.items ?? [])
|
const plannedItems = computed<any[]>(() => selectedRequest.value?.items ?? [])
|
||||||
|
|
||||||
// 扫码进度
|
// ============================================================
|
||||||
|
// ★ 按执行模式分流(与后端 scrap_mode 对齐)
|
||||||
|
//
|
||||||
|
// scan —— 需扫码:库存行、在管不良品(实物在库、有可扫标识)
|
||||||
|
// auto —— 免扫码:借出未还(东西在借用人手上,物理上不可能扫),
|
||||||
|
// 执行时按批准数量直接核销
|
||||||
|
//
|
||||||
|
// 存量单据没有 scrap_mode 字段,回落为 scan —— 行为与改造前完全一致。
|
||||||
|
// ============================================================
|
||||||
|
const modeOf = (it: any) => (it?.scrap_mode === 'auto' ? 'auto' : 'scan')
|
||||||
|
const scanItems = computed<any[]>(() => plannedItems.value.filter((it: any) => modeOf(it) === 'scan'))
|
||||||
|
const autoItems = computed<any[]>(() => plannedItems.value.filter((it: any) => modeOf(it) === 'auto'))
|
||||||
|
|
||||||
|
// 扫码进度 —— ★ 只统计需扫码的项。
|
||||||
|
// 若把 auto 项也算进来,进度条永远达不到 100%,会让操作员误以为「没扫完」。
|
||||||
const scanTotalQty = computed(() =>
|
const scanTotalQty = computed(() =>
|
||||||
plannedItems.value.reduce((sum: number, it: any) => sum + (Number(it.scrap_qty) || 0), 0)
|
scanItems.value.reduce((sum: number, it: any) => sum + (Number(it.scrap_qty) || 0), 0)
|
||||||
)
|
)
|
||||||
const scanScannedQty = computed(() =>
|
const scanScannedQty = computed(() =>
|
||||||
cartItems.value.reduce((sum: number, it: any) => sum + (Number(it.scrap_quantity) || 0), 0)
|
cartItems.value.reduce((sum: number, it: any) => sum + (Number(it.scrap_quantity) || 0), 0)
|
||||||
)
|
)
|
||||||
const scanScannedTypes = computed(() => cartItems.value.length)
|
const scanScannedTypes = computed(() => cartItems.value.length)
|
||||||
const scanTotalTypes = computed(() => plannedItems.value.length)
|
const scanTotalTypes = computed(() => scanItems.value.length)
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ★ SKU 主键匹配(与后端 _match_key 完全同口径)
|
// ★ SKU 主键匹配(与后端 _match_key 完全同口径)
|
||||||
//
|
//
|
||||||
// SKU 在同一库存表内唯一、且不存在跨表重名,故以 SKU 作为扫码匹配的唯一标识。
|
// ★ 匹配键必须带来源表。原实现只用 SKU,其前提「SKU 跨表无重名」在引入
|
||||||
|
// 逆向物流后不再成立:在管不良品台账的 SKU 是从原库存行**复制**的,
|
||||||
|
// 故 trans_defective_goods#N 与其源 stock_buy#M 的 SKU 必然相同。
|
||||||
|
// 若同一张单同时含两者,不带来源就会串键 —— 扫码量会算到错误对象上。
|
||||||
|
// 纯库存单两侧同源、键仍匹配,对既有流程零行为变更。
|
||||||
|
//
|
||||||
// 个别历史库存行 SKU 为空,这类行回退为 source_table + stock_id 复合键;
|
// 个别历史库存行 SKU 为空,这类行回退为 source_table + stock_id 复合键;
|
||||||
// 前缀区分(sku: / row:)确保空 SKU 行绝不会与正常 SKU 串键。
|
// 前缀区分(sku: / row:)确保两种键绝不会互相串。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const normSku = (sku: any) => String(sku ?? '').trim()
|
const normSku = (sku: any) => String(sku ?? '').trim()
|
||||||
|
|
||||||
const matchKey = (sourceTable: any, stockId: any, sku: any) => {
|
const matchKey = (sourceTable: any, stockId: any, sku: any) => {
|
||||||
const s = normSku(sku)
|
const s = normSku(sku)
|
||||||
return s ? `sku:${s}` : `row:${String(sourceTable)}#${String(stockId)}`
|
return s
|
||||||
|
? `sku:${String(sourceTable)}:${s}`
|
||||||
|
: `row:${String(sourceTable)}#${String(stockId)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按匹配键找批准明细行
|
// 按匹配键找批准明细行
|
||||||
@ -396,7 +453,14 @@ const unscannedList = computed(() =>
|
|||||||
const unscannedCount = computed(() => unscannedList.value.length)
|
const unscannedCount = computed(() => unscannedList.value.length)
|
||||||
|
|
||||||
const sourceLabel = (st: string) =>
|
const sourceLabel = (st: string) =>
|
||||||
({ stock_buy: '采购件', stock_semi: '半成品', stock_product: '成品' } as any)[st] || st || '-'
|
({
|
||||||
|
stock_buy: '采购件',
|
||||||
|
stock_semi: '半成品',
|
||||||
|
stock_product: '成品',
|
||||||
|
// 逆向物流两类来源(与后端 scrap_sources 的 label 对齐)
|
||||||
|
trans_defective_goods: '在管不良品',
|
||||||
|
trans_borrow: '借出未还',
|
||||||
|
} as any)[st] || st || '-'
|
||||||
|
|
||||||
// --- 加载已批准的报废申请单 ---
|
// --- 加载已批准的报废申请单 ---
|
||||||
const loadApprovalRequests = async () => {
|
const loadApprovalRequests = async () => {
|
||||||
@ -588,7 +652,11 @@ const submitForm = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!selectedRequest.value) return ElMessage.warning('请先选择要执行的报废申请单')
|
if (!selectedRequest.value) return ElMessage.warning('请先选择要执行的报废申请单')
|
||||||
if (cartItems.value.length === 0) return ElMessage.warning('请先扫码添加报废物料')
|
// ★ 守卫放宽为「购物车与免扫码明细都为空才拦」——
|
||||||
|
// 纯免扫码单(借出未还)不存在可扫的实物,购物车恒为空。
|
||||||
|
if (cartItems.value.length === 0 && autoItems.value.length === 0) {
|
||||||
|
return ElMessage.warning('请先扫码添加报废物料')
|
||||||
|
}
|
||||||
|
|
||||||
// 逐项复核批准量
|
// 逐项复核批准量
|
||||||
for (const item of cartItems.value) {
|
for (const item of cartItems.value) {
|
||||||
@ -603,8 +671,14 @@ const submitForm = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// ★ 确认框必须点明「另有 N 项免扫码项将一并执行」——
|
||||||
|
// 否则操作员会以为只报废了购物车里扫到的那些,导致误判。
|
||||||
|
const autoNote = autoItems.value.length > 0
|
||||||
|
? `\n另有 ${autoItems.value.length} 项(借出未还)将按批准数量自动执行,无需扫码。`
|
||||||
|
: ''
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确认按实扫数量执行报废吗?\n申请单:${selectedRequest.value.request_no}\n共 ${cartItems.value.length} 项 / ${scanScannedQty.value} 件\n\n执行后不可撤销。`,
|
`确认按实扫数量执行报废吗?\n申请单:${selectedRequest.value.request_no}\n`
|
||||||
|
+ `共 ${cartItems.value.length} 项 / ${scanScannedQty.value} 件${autoNote}\n\n执行后不可撤销。`,
|
||||||
'执行确认',
|
'执行确认',
|
||||||
{ confirmButtonText: '确认执行', cancelButtonText: '取消', type: 'warning' }
|
{ confirmButtonText: '确认执行', cancelButtonText: '取消', type: 'warning' }
|
||||||
)
|
)
|
||||||
|
|||||||
@ -132,7 +132,7 @@
|
|||||||
type="danger" link size="small"
|
type="danger" link size="small"
|
||||||
@click="openScrapDialog(row)"
|
@click="openScrapDialog(row)"
|
||||||
>
|
>
|
||||||
报废销毁
|
申请报废
|
||||||
</el-button>
|
</el-button>
|
||||||
<span v-if="!canRestock && !canScrap" class="no-perm">无处置权限</span>
|
<span v-if="!canRestock && !canScrap" class="no-perm">无处置权限</span>
|
||||||
</template>
|
</template>
|
||||||
@ -198,11 +198,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- ============ 报废销毁 ============ -->
|
<!-- ============ 申请报废 ============ -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="scrapDialog.visible"
|
v-model="scrapDialog.visible"
|
||||||
title="报废销毁"
|
title="申请报废"
|
||||||
width="460px"
|
width="480px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@closed="resetScrapDialog"
|
@closed="resetScrapDialog"
|
||||||
>
|
>
|
||||||
@ -230,17 +230,29 @@
|
|||||||
:rows="3"
|
:rows="3"
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="请填写报废原因(必填)"
|
placeholder="请填写报废原因(必填,审批人据此判断)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="指定审批人" required>
|
||||||
|
<el-select
|
||||||
|
v-model="scrapDialog.form.approver_id"
|
||||||
|
placeholder="请选择审批人"
|
||||||
|
style="width: 100%"
|
||||||
|
filterable
|
||||||
|
>
|
||||||
|
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="dlg-tip dlg-danger">
|
<!-- ★ 必须说明「在管量不会立即变化」,否则用户会以为没生效而重复提交 -->
|
||||||
报废后实物视为已销毁,不可撤销;同时会写入报废台账。
|
<div class="dlg-tip dlg-warn">
|
||||||
|
提交后进入审批流程,<b>在管数量不会立即扣减</b>;
|
||||||
|
待审批人通过、库管执行报废后才会扣减并写入报废台账。
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="scrapDialog.visible = false">取消</el-button>
|
<el-button @click="scrapDialog.visible = false">取消</el-button>
|
||||||
<el-button type="danger" :loading="scrapDialog.submitting" @click="submitScrap">
|
<el-button type="danger" :loading="scrapDialog.submitting" @click="submitScrap">
|
||||||
确定报废
|
提交报废申请
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@ -250,7 +262,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, reactive, onMounted } from 'vue'
|
import { ref, computed, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getDefectiveList, restockDefective, scrapDefective } from '@/api/inbound/return'
|
import { getDefectiveList, restockDefective, submitDefectiveScrapRequest } from '@/api/inbound/return'
|
||||||
|
import { getApproversList } from '@/api/auth'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { formatQty, normalizeQty } from '@/utils/format'
|
import { formatQty, normalizeQty } from '@/utils/format'
|
||||||
import CompanySelector from '@/components/CompanySelector.vue'
|
import CompanySelector from '@/components/CompanySelector.vue'
|
||||||
@ -388,20 +401,33 @@ const submitRestock = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// ★ 报废销毁
|
// ★ 申请报废(需审批人审批,通过后由库管执行)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
const approvers = ref<any[]>([])
|
||||||
|
|
||||||
|
const loadApprovers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getApproversList()
|
||||||
|
approvers.value = res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[申请报废] 审批人列表加载失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const scrapDialog = reactive({
|
const scrapDialog = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
row: null as any,
|
row: null as any,
|
||||||
form: { scrap_qty: 0, reason: '' },
|
form: { scrap_qty: 0, reason: '', approver_id: null as number | null },
|
||||||
})
|
})
|
||||||
|
|
||||||
const openScrapDialog = (row: any) => {
|
const openScrapDialog = (row: any) => {
|
||||||
scrapDialog.row = row
|
scrapDialog.row = row
|
||||||
scrapDialog.form.scrap_qty = normalizeQty(row?.remaining_qty)
|
scrapDialog.form.scrap_qty = normalizeQty(row?.remaining_qty)
|
||||||
scrapDialog.form.reason = ''
|
scrapDialog.form.reason = ''
|
||||||
|
scrapDialog.form.approver_id = null
|
||||||
scrapDialog.visible = true
|
scrapDialog.visible = true
|
||||||
|
loadApprovers()
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetScrapDialog = () => {
|
const resetScrapDialog = () => {
|
||||||
@ -409,6 +435,7 @@ const resetScrapDialog = () => {
|
|||||||
scrapDialog.submitting = false
|
scrapDialog.submitting = false
|
||||||
scrapDialog.form.scrap_qty = 0
|
scrapDialog.form.scrap_qty = 0
|
||||||
scrapDialog.form.reason = ''
|
scrapDialog.form.reason = ''
|
||||||
|
scrapDialog.form.approver_id = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitScrap = async () => {
|
const submitScrap = async () => {
|
||||||
@ -430,18 +457,26 @@ const submitScrap = async () => {
|
|||||||
ElMessage.warning('请填写报废原因')
|
ElMessage.warning('请填写报废原因')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!scrapDialog.form.approver_id) {
|
||||||
|
ElMessage.warning('请选择审批人')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
scrapDialog.submitting = true
|
scrapDialog.submitting = true
|
||||||
try {
|
try {
|
||||||
const res = await scrapDefective(scrapDialog.row.id, {
|
const res = await submitDefectiveScrapRequest(scrapDialog.row.id, {
|
||||||
scrap_qty: qty,
|
scrap_qty: qty,
|
||||||
reason,
|
reason,
|
||||||
|
approver_id: scrapDialog.form.approver_id,
|
||||||
})
|
})
|
||||||
ElMessage.success(res?.msg || '报废成功')
|
// ★ 提示里必须点明「在管量未变」—— 申请不预占,否则用户会以为没生效而重复提交
|
||||||
|
ElMessage.success(
|
||||||
|
`${res?.msg || '报废申请已提交'}:在管数量将在审批通过并执行后才扣减`,
|
||||||
|
)
|
||||||
scrapDialog.visible = false
|
scrapDialog.visible = false
|
||||||
await fetchData()
|
await fetchData()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[报废销毁] 失败', e)
|
console.error('[申请报废] 失败', e)
|
||||||
} finally {
|
} finally {
|
||||||
scrapDialog.submitting = false
|
scrapDialog.submitting = false
|
||||||
}
|
}
|
||||||
@ -482,4 +517,9 @@ onMounted(() => {
|
|||||||
.dlg-danger {
|
.dlg-danger {
|
||||||
color: #F56C6C;
|
color: #F56C6C;
|
||||||
}
|
}
|
||||||
|
/* 流程提示:告知「提交不等于立即生效」,避免重复提交 */
|
||||||
|
.dlg-warn {
|
||||||
|
color: #E6A23C;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -199,7 +199,7 @@
|
|||||||
v-if="row.status === 'borrowed' || row.status === 'partial_returned'"
|
v-if="row.status === 'borrowed' || row.status === 'partial_returned'"
|
||||||
type="danger" link size="small"
|
type="danger" link size="small"
|
||||||
@click="openScrapDialog(row)"
|
@click="openScrapDialog(row)"
|
||||||
>报废</el-button>
|
>申请报废</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -212,10 +212,10 @@
|
|||||||
style="margin-top:10px; text-align:right"
|
style="margin-top:10px; text-align:right"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- ★ 借库报废弹窗 -->
|
<!-- ★ 借库报废**申请**弹窗(需审批,不再直报) -->
|
||||||
<el-dialog v-model="scrapDialogVisible" title="借库报废(未归还)" width="650px" destroy-on-close>
|
<el-dialog v-model="scrapDialogVisible" title="申请借库报废(未归还)" width="650px" destroy-on-close>
|
||||||
<el-alert
|
<el-alert
|
||||||
title="报废后该笔借出不再追讨归还,并从总库存中扣除。此操作不可恢复,请谨慎确认。"
|
title="提交后进入审批流程:审批通过并执行后,该笔借出才不再追讨归还,并从总库存中扣除。"
|
||||||
type="warning"
|
type="warning"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
@ -245,10 +245,15 @@
|
|||||||
placeholder="请填写报废原因,如:物品丢失 / 损坏无法归还"
|
placeholder="请填写报废原因,如:物品丢失 / 损坏无法归还"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="审批人" required>
|
||||||
|
<el-select v-model="scrapApproverId" placeholder="请选择审批人" style="width: 100%" filterable>
|
||||||
|
<el-option v-for="u in approvers" :key="u.id" :label="u.username" :value="u.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="scrapDialogVisible = false">取消</el-button>
|
<el-button @click="scrapDialogVisible = false">取消</el-button>
|
||||||
<el-button type="danger" :loading="scrapSubmitting" @click="confirmScrap">确认报废</el-button>
|
<el-button type="danger" :loading="scrapSubmitting" @click="confirmScrap">提交报废申请</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@ -262,6 +267,8 @@ import dayjs from 'dayjs'
|
|||||||
import 'dayjs/locale/zh-cn'
|
import 'dayjs/locale/zh-cn'
|
||||||
dayjs.locale('zh-cn')
|
dayjs.locale('zh-cn')
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { getApproversList } from '@/api/auth'
|
||||||
|
import { submitBorrowScrapRequest } from '@/api/transaction'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@ -365,11 +372,10 @@ const resetAdvancedFilter = () => {
|
|||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ 借库报废相关
|
// ★ 借库报废**申请**相关(需审批,不再直报)
|
||||||
const canScrap = computed(() =>
|
const canScrap = computed(() =>
|
||||||
userStore.role === 'SUPER_ADMIN' ||
|
userStore.role === 'SUPER_ADMIN' ||
|
||||||
userStore.hasPermission('op_return:operation') ||
|
userStore.hasPermission('op_return:operation')
|
||||||
userStore.username === 'IRIS'
|
|
||||||
)
|
)
|
||||||
const scrapDialogVisible = ref(false)
|
const scrapDialogVisible = ref(false)
|
||||||
const scrapSubmitting = ref(false)
|
const scrapSubmitting = ref(false)
|
||||||
@ -377,15 +383,28 @@ const scrapBorrowNo = ref('')
|
|||||||
const scrapCandidates = ref<any[]>([])
|
const scrapCandidates = ref<any[]>([])
|
||||||
const scrapSelected = ref<any[]>([])
|
const scrapSelected = ref<any[]>([])
|
||||||
const scrapReason = ref('')
|
const scrapReason = ref('')
|
||||||
|
const scrapApproverId = ref<number | null>(null)
|
||||||
|
const approvers = ref<any[]>([])
|
||||||
|
|
||||||
// 打开报废弹窗:收集该单号下所有未归还明细
|
const loadApprovers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getApproversList()
|
||||||
|
approvers.value = res.data || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[借库报废申请] 审批人列表加载失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开报废申请弹窗:收集该单号下所有未归还明细
|
||||||
const openScrapDialog = (row: any) => {
|
const openScrapDialog = (row: any) => {
|
||||||
const unreturned = (row.children || []).filter((c: any) => (c.pending_quantity || 0) > 0)
|
const unreturned = (row.children || []).filter((c: any) => (c.pending_quantity || 0) > 0)
|
||||||
scrapBorrowNo.value = row.borrow_no
|
scrapBorrowNo.value = row.borrow_no
|
||||||
scrapCandidates.value = unreturned
|
scrapCandidates.value = unreturned
|
||||||
scrapSelected.value = []
|
scrapSelected.value = []
|
||||||
scrapReason.value = ''
|
scrapReason.value = ''
|
||||||
|
scrapApproverId.value = null
|
||||||
scrapDialogVisible.value = true
|
scrapDialogVisible.value = true
|
||||||
|
loadApprovers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 勾选变更
|
// 勾选变更
|
||||||
@ -393,16 +412,18 @@ const handleScrapSelection = (rows: any[]) => {
|
|||||||
scrapSelected.value = rows
|
scrapSelected.value = rows
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认报废
|
// 提交报废申请
|
||||||
const confirmScrap = async () => {
|
const confirmScrap = async () => {
|
||||||
if (scrapSelected.value.length === 0) return ElMessage.warning('请至少选择一条要报废的借出记录')
|
if (scrapSelected.value.length === 0) return ElMessage.warning('请至少选择一条要申请报废的借出记录')
|
||||||
if (!scrapReason.value.trim()) return ElMessage.warning('请填写报废原因')
|
if (!scrapReason.value.trim()) return ElMessage.warning('请填写报废原因')
|
||||||
|
if (!scrapApproverId.value) return ElMessage.warning('请选择审批人')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
`确定报废 ${scrapSelected.value.length} 条借出记录吗?\n\n报废后不再追讨归还,并从总库存中扣除。此操作不可恢复!`,
|
`确定对 ${scrapSelected.value.length} 条借出记录提交报废申请吗?\n\n`
|
||||||
'⚠️ 报废确认',
|
+ '提交后进入审批流程,审批通过并执行后才会扣减总库存。',
|
||||||
{ confirmButtonText: '确认报废', cancelButtonText: '取消', type: 'warning' }
|
'报废申请确认',
|
||||||
|
{ confirmButtonText: '提交申请', cancelButtonText: '取消', type: 'warning' }
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return // 用户取消
|
return // 用户取消
|
||||||
@ -411,16 +432,19 @@ const confirmScrap = async () => {
|
|||||||
scrapSubmitting.value = true
|
scrapSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
const recordIds = scrapSelected.value.map((c: any) => c.id)
|
const recordIds = scrapSelected.value.map((c: any) => c.id)
|
||||||
await request({
|
const res = await submitBorrowScrapRequest({
|
||||||
url: '/v1/transactions/borrow/scrap',
|
record_ids: recordIds,
|
||||||
method: 'post',
|
reason: scrapReason.value,
|
||||||
data: { record_ids: recordIds, reason: scrapReason.value }
|
approver_id: scrapApproverId.value,
|
||||||
})
|
})
|
||||||
ElMessage.success(`已报废 ${recordIds.length} 条借出记录`)
|
// ★ 提示里点明「尚未扣减」—— 申请不立即改状态,否则用户会以为没生效而重复提交
|
||||||
|
ElMessage.success(
|
||||||
|
`${res?.msg || '报废申请已提交'}:审批通过并执行后才会标记为已报废`,
|
||||||
|
)
|
||||||
scrapDialogVisible.value = false
|
scrapDialogVisible.value = false
|
||||||
fetchData()
|
fetchData()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ElMessage.error(err?.msg || err?.message || '报废失败')
|
ElMessage.error(err?.msg || err?.message || '提交报废申请失败')
|
||||||
} finally {
|
} finally {
|
||||||
scrapSubmitting.value = false
|
scrapSubmitting.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user