feat(return): 前端退回入口与不良品在管台账看板
出库记录页(views/outbound/index.vue): - 明细行新增「退回」列,returnable_quantity <= 0 时按钮置灰,行内显示「已退 N」 - 对话框展示 原出库数量 / 已退回数量 / 本次可退最大,默认带入可退最大值 - 退回类型用下拉单选:良品(加回库存)/ 不良品(转入异常待处理) - 退回原因必填,提交前三重校验;按钮 :loading + 函数内 submitting 双保险防抖 - 成功后刷新列表。错误提示不重复弹——request 拦截器对 HTTP 400 已取 data.msg 展示,对话框 catch 只收尾 配套后端(services/outbound_service.py): - get_grouped_list 的出库明细补 id / returned_quantity / returnable_quantity。 原先明细不带 id,退回接口无从指定 outbound_id 新增页面(views/stock/defective/index.vue,路由 /inventory/defective): - 展示 物料名称/规格/SKU/退回时间/操作人/退回总数/剩余待处理/状态 - 顶部 alert 提示在管总量,并明确「这批实物不在库存表中,盘点请以本台账 为准」——在管坏件对盘点不可见是本方案的固有盲区,必须在页面上主动提醒 - 仅对 remaining_qty > 0 的行提供「修复回库」「报废销毁」,终态行显示已结案 - 两个弹窗均带数量上限约束与 loading 防抖,成功后刷新 新增 api/inbound/return.ts 承载四个逆向物流接口。 侧边栏由 router/index.ts 驱动,加路由即入菜单;注意侧边栏只过滤 meta.hidden、不看 meta.permission,故菜单对所有角色可见,访问控制由后端 接口负责(与既有「维修管理」一致)。
This commit is contained in:
@ -120,6 +120,28 @@
|
||||
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- ★ 原单退回:良品加回库存 / 不良品转入异常待处理台账。
|
||||
已退满的行(returnable_quantity <= 0)按钮置灰不可点。 -->
|
||||
<el-table-column label="退回" width="150" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="canReturn">
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
size="small"
|
||||
:disabled="!row.returnable_quantity || row.returnable_quantity <= 0"
|
||||
@click="openReturnDialog(row)"
|
||||
>
|
||||
退回
|
||||
</el-button>
|
||||
<span v-if="row.returned_quantity > 0" class="returned-hint">
|
||||
已退 {{ row.returned_quantity }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="returned-hint">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
@ -183,18 +205,94 @@
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ★ 原单退回对话框 -->
|
||||
<el-dialog
|
||||
v-model="returnDialog.visible"
|
||||
title="原单退回"
|
||||
width="540px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetReturnDialog"
|
||||
>
|
||||
<el-form :model="returnDialog.form" label-width="120px">
|
||||
<el-form-item label="物料">
|
||||
<span class="dialog-text">
|
||||
{{ returnDialog.row?.name || '-' }}
|
||||
<span class="dialog-sub">/ {{ returnDialog.row?.sku || '-' }}</span>
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="原出库数量">
|
||||
<span class="dialog-text">{{ returnDialog.row?.quantity ?? 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="已退回数量">
|
||||
<span class="dialog-text">{{ returnDialog.row?.returned_quantity ?? 0 }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="本次可退最大">
|
||||
<span class="dialog-text dialog-strong">
|
||||
{{ returnDialog.row?.returnable_quantity ?? 0 }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="本次退回数量" required>
|
||||
<el-input-number
|
||||
v-model="returnDialog.form.return_qty"
|
||||
:min="0"
|
||||
:max="returnDialog.row?.returnable_quantity || 0"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="退回类型" required>
|
||||
<el-select v-model="returnDialog.form.is_defective" style="width: 260px">
|
||||
<el-option :value="false" label="良品(加回库存)" />
|
||||
<el-option :value="true" label="不良品(转入异常待处理)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="退回原因" required>
|
||||
<el-input
|
||||
v-model="returnDialog.form.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请填写退回原因(必填)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="returnDialog.visible = false">取消</el-button>
|
||||
<!-- loading 兼作前端防抖:提交期间按钮不可再点 -->
|
||||
<el-button type="primary" :loading="returnDialog.submitting" @click="submitReturn">
|
||||
确定提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, onMounted, reactive, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOutboundList } from '@/api/outbound'
|
||||
import { returnFromOutbound } from '@/api/inbound/return'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import CompanySelector from '@/components/CompanySelector.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 退回按钮的可见性。后端同样会校验权限(inventory_stocktake:operation),
|
||||
// 前端只是不给无权限用户显示一个必然失败的按钮。
|
||||
const canReturn = computed(() =>
|
||||
userStore.role === 'SUPER_ADMIN'
|
||||
|| userStore.username === 'IRIS'
|
||||
|| userStore.hasPermission('inventory_stocktake:operation')
|
||||
)
|
||||
|
||||
// 防抖定时器
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@ -381,6 +479,77 @@ const getTagType = (type: string) => {
|
||||
return map[type] || ''
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ★ 原单退回
|
||||
// ============================================================
|
||||
const returnDialog = reactive({
|
||||
visible: false,
|
||||
submitting: false,
|
||||
row: null as any,
|
||||
form: {
|
||||
return_qty: 1,
|
||||
is_defective: false,
|
||||
reason: '',
|
||||
},
|
||||
})
|
||||
|
||||
const openReturnDialog = (row: any) => {
|
||||
returnDialog.row = row
|
||||
// 默认带入「本次可退最大」,业务上整行退回最常见;用户可再调小做部分退回
|
||||
returnDialog.form.return_qty = Number(row?.returnable_quantity || 0)
|
||||
returnDialog.form.is_defective = false
|
||||
returnDialog.form.reason = ''
|
||||
returnDialog.visible = true
|
||||
}
|
||||
|
||||
const resetReturnDialog = () => {
|
||||
returnDialog.row = null
|
||||
returnDialog.submitting = false
|
||||
returnDialog.form.return_qty = 1
|
||||
returnDialog.form.is_defective = false
|
||||
returnDialog.form.reason = ''
|
||||
}
|
||||
|
||||
const submitReturn = async () => {
|
||||
if (returnDialog.submitting) return // 双保险:即便按钮 loading 被绕过也不重复提交
|
||||
|
||||
const maxQty = Number(returnDialog.row?.returnable_quantity || 0)
|
||||
const qty = Number(returnDialog.form.return_qty || 0)
|
||||
const reason = (returnDialog.form.reason || '').trim()
|
||||
|
||||
if (!qty || qty <= 0) {
|
||||
ElMessage.warning('请填写本次退回数量')
|
||||
return
|
||||
}
|
||||
if (qty > maxQty) {
|
||||
ElMessage.warning(`本次退回数量不能超过可退最大数量 ${maxQty}`)
|
||||
return
|
||||
}
|
||||
if (!reason) {
|
||||
ElMessage.warning('请填写退回原因')
|
||||
return
|
||||
}
|
||||
|
||||
returnDialog.submitting = true
|
||||
try {
|
||||
const res = await returnFromOutbound({
|
||||
outbound_id: returnDialog.row.id,
|
||||
return_qty: qty,
|
||||
is_defective: returnDialog.form.is_defective,
|
||||
reason,
|
||||
})
|
||||
ElMessage.success(res?.msg || '退回成功')
|
||||
returnDialog.visible = false
|
||||
// 刷新列表:退回额度与库存状态都已变化
|
||||
await fetchData()
|
||||
} catch (e) {
|
||||
// 错误提示由 request 拦截器统一弹出(400 会展示后端业务文案),此处只收尾
|
||||
console.error('[退回] 失败', e)
|
||||
} finally {
|
||||
returnDialog.submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
@ -419,6 +588,24 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* ★ 原单退回:明细行的「已退 N」提示与对话框文案样式 */
|
||||
.returned-hint {
|
||||
color: #E6A23C;
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.dialog-text {
|
||||
color: #303133;
|
||||
}
|
||||
.dialog-sub {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dialog-strong {
|
||||
color: #409EFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user