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:
86
inventory-web/src/api/inbound/return.ts
Normal file
86
inventory-web/src/api/inbound/return.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 逆向物流(原单退回 + 不良品在管台账)
|
||||||
|
//
|
||||||
|
// 后端路由见 inventory-backend/app/api/v1/inbound/stock.py
|
||||||
|
// 设计要点:
|
||||||
|
// · 良品退回 → 直接加回原库存行
|
||||||
|
// · 不良品退回 → 不进库存表,转入 trans_defective_goods 在管台账,
|
||||||
|
// 修好后回库 / 无法维修则报废销毁
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原单退回。
|
||||||
|
*
|
||||||
|
* @param data {
|
||||||
|
* outbound_id: number 必填,trans_outbound.id(出库**明细行**主键,非单号)
|
||||||
|
* return_qty: number 必填,本次退回数量(>0)
|
||||||
|
* is_defective: boolean 必填,true=不良品转在管台账,false=良品加回库存
|
||||||
|
* reason: string 可选,退回原因
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function returnFromOutbound(data: {
|
||||||
|
outbound_id: number
|
||||||
|
return_qty: number
|
||||||
|
is_defective: boolean
|
||||||
|
reason?: string
|
||||||
|
}) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/stock/return-from-outbound',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不良品在管台账分页查询。
|
||||||
|
*
|
||||||
|
* @param params { page, page_size, status?, keyword?, start_date?, end_date? }
|
||||||
|
* status 传 '全部' 或留空表示不过滤
|
||||||
|
*/
|
||||||
|
export function getDefectiveList(params: {
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
status?: string
|
||||||
|
keyword?: string
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
company_name?: string
|
||||||
|
}) {
|
||||||
|
return request({
|
||||||
|
url: '/inbound/stock/defective',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修复回库:把修好的数量加回原库存行。
|
||||||
|
*
|
||||||
|
* @param id 在管台账 id
|
||||||
|
* @param data { restock_qty?: number, remark?: string }
|
||||||
|
* restock_qty 缺省 = 全部剩余在管量
|
||||||
|
*/
|
||||||
|
export function restockDefective(id: number, data: { restock_qty?: number; remark?: string }) {
|
||||||
|
return request({
|
||||||
|
url: `/inbound/stock/defective/${id}/restock`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报废销毁:在管坏件确认无法维修时销毁。
|
||||||
|
*
|
||||||
|
* @param id 在管台账 id
|
||||||
|
* @param data { scrap_qty?: number, reason: string }
|
||||||
|
* reason 必填;scrap_qty 缺省 = 全部剩余在管量
|
||||||
|
*/
|
||||||
|
export function scrapDefective(id: number, data: { scrap_qty?: number; reason: string }) {
|
||||||
|
return request({
|
||||||
|
url: `/inbound/stock/defective/${id}/scrap`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -106,6 +106,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'RepairManagement',
|
name: 'RepairManagement',
|
||||||
component: () => import('@/views/stock/inbound/repair.vue'),
|
component: () => import('@/views/stock/inbound/repair.vue'),
|
||||||
meta: { title: '维修管理', permission: 'inbound_repair' }
|
meta: { title: '维修管理', permission: 'inbound_repair' }
|
||||||
|
},
|
||||||
|
// ★ 不良品在管台账(逆向物流):退回的坏件不入库存表,
|
||||||
|
// 实物在仓但不在库存行中,故单独成页承载其去向与处置。
|
||||||
|
{
|
||||||
|
path: 'defective',
|
||||||
|
name: 'DefectiveGoods',
|
||||||
|
component: () => import('@/views/stock/defective/index.vue'),
|
||||||
|
meta: { title: '不良品在管台账', permission: 'inventory_stocktake' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -120,6 +120,28 @@
|
|||||||
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -183,18 +205,94 @@
|
|||||||
@current-change="handlePageChange"
|
@current-change="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { getOutboundList } from '@/api/outbound'
|
||||||
|
import { returnFromOutbound } from '@/api/inbound/return'
|
||||||
import { Picture } from '@element-plus/icons-vue'
|
import { Picture } from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import CompanySelector from '@/components/CompanySelector.vue'
|
import CompanySelector from '@/components/CompanySelector.vue'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
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
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
@ -381,6 +479,77 @@ const getTagType = (type: string) => {
|
|||||||
return map[type] || ''
|
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(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
@ -419,6 +588,24 @@ onBeforeUnmount(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2px 0;
|
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 {
|
.image-slot {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
448
inventory-web/src/views/stock/defective/index.vue
Normal file
448
inventory-web/src/views/stock/defective/index.vue
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 顶部汇总:在管坏件是「不在库存表里」的实物,用这个数字提醒别漏盘 -->
|
||||||
|
<el-alert
|
||||||
|
v-if="pendingTotal > 0"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 12px;"
|
||||||
|
>
|
||||||
|
当前在管不良品剩余待处理:<b>{{ pendingTotal }}</b>
|
||||||
|
件。这批实物不在库存表中,盘点时请以本台账为准。
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<!-- 筛选区 -->
|
||||||
|
<el-form :inline="true" class="filter-form" @submit.prevent>
|
||||||
|
<el-form-item>
|
||||||
|
<CompanySelector v-model="query.company_name" @change="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="query.status" placeholder="全部" style="width: 150px" clearable>
|
||||||
|
<el-option label="全部" value="全部" />
|
||||||
|
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="搜索">
|
||||||
|
<el-input
|
||||||
|
v-model="query.keyword"
|
||||||
|
placeholder="物料名称 / SKU / 规格"
|
||||||
|
style="width: 240px;"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
@clear="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="退回时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 260px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="resetFilter">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="list"
|
||||||
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
style="width: 100%; margin-top: 16px;"
|
||||||
|
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="material_name" label="物料名称" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.material_name">{{ row.material_name }}</span>
|
||||||
|
<span v-else style="color:#c0c4cc">(原库存行已删除)</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.spec_model">{{ row.spec_model }}</span>
|
||||||
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="sku" label="SKU" width="150" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column label="退回时间" width="170" align="center">
|
||||||
|
<template #default="{ row }">{{ row.created_at || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="operator" label="操作人" width="110" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.operator">{{ row.operator }}</span>
|
||||||
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="quantity" label="退回总数" width="100" align="right" />
|
||||||
|
|
||||||
|
<el-table-column label="剩余待处理" width="110" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :style="{ color: row.remaining_qty > 0 ? '#E6A23C' : '#909399', fontWeight: 'bold' }">
|
||||||
|
{{ row.remaining_qty }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusTagType(row.status)">{{ row.status }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<!-- ★ 仅在还有在管量时提供处置入口:终态(已回库/已报废/已闭环)
|
||||||
|
的 remaining_qty 已归零,再操作只会拿到后端 400 -->
|
||||||
|
<template v-if="row.remaining_qty > 0">
|
||||||
|
<el-button type="success" link size="small" @click="openRestockDialog(row)">
|
||||||
|
修复回库
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" link size="small" @click="openScrapDialog(row)">
|
||||||
|
报废销毁
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<span v-else style="color: #c0c4cc; font-size: 12px;">已结案</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div style="margin-top: 20px; text-align: right;">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
:page-size="query.page_size"
|
||||||
|
:current-page="query.page"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ 修复回库 ============ -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="restockDialog.visible"
|
||||||
|
title="修复回库"
|
||||||
|
width="460px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="resetRestockDialog"
|
||||||
|
>
|
||||||
|
<el-form label-width="110px">
|
||||||
|
<el-form-item label="物料">
|
||||||
|
<span class="dlg-text">{{ restockDialog.row?.material_name || '-' }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="在管数量">
|
||||||
|
<span class="dlg-text dlg-strong">{{ restockDialog.row?.remaining_qty ?? 0 }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="本次回库数量" required>
|
||||||
|
<el-input-number
|
||||||
|
v-model="restockDialog.form.restock_qty"
|
||||||
|
:min="0"
|
||||||
|
:max="restockDialog.row?.remaining_qty || 0"
|
||||||
|
:precision="4"
|
||||||
|
:step="1"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="restockDialog.form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="如:已更换主板(可选)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="dlg-tip">回库后数量将加回原库存行的实物数与可用数,状态变为「在库」。</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="restockDialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="restockDialog.submitting" @click="submitRestock">
|
||||||
|
确定回库
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- ============ 报废销毁 ============ -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="scrapDialog.visible"
|
||||||
|
title="报废销毁"
|
||||||
|
width="460px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="resetScrapDialog"
|
||||||
|
>
|
||||||
|
<el-form label-width="110px">
|
||||||
|
<el-form-item label="物料">
|
||||||
|
<span class="dlg-text">{{ scrapDialog.row?.material_name || '-' }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="在管数量">
|
||||||
|
<span class="dlg-text dlg-strong">{{ scrapDialog.row?.remaining_qty ?? 0 }}</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="本次报废数量" required>
|
||||||
|
<el-input-number
|
||||||
|
v-model="scrapDialog.form.scrap_qty"
|
||||||
|
:min="0"
|
||||||
|
:max="scrapDialog.row?.remaining_qty || 0"
|
||||||
|
:precision="4"
|
||||||
|
:step="1"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报废原因" required>
|
||||||
|
<el-input
|
||||||
|
v-model="scrapDialog.form.reason"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请填写报废原因(必填)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="dlg-tip dlg-danger">
|
||||||
|
报废后实物视为已销毁,不可撤销;同时会写入报废台账。
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="scrapDialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="danger" :loading="scrapDialog.submitting" @click="submitScrap">
|
||||||
|
确定报废
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getDefectiveList, restockDefective, scrapDefective } from '@/api/inbound/return'
|
||||||
|
import CompanySelector from '@/components/CompanySelector.vue'
|
||||||
|
|
||||||
|
// 与后端 app/models/transaction.py 的 VALID_DEFECTIVE_STATUSES 保持一致
|
||||||
|
const statusOptions = ['待处理', '处理中', '已回库', '已报废', '已闭环']
|
||||||
|
|
||||||
|
const statusTagType = (status: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
'待处理': 'warning',
|
||||||
|
'处理中': 'primary',
|
||||||
|
'已回库': 'success',
|
||||||
|
'已报废': 'danger',
|
||||||
|
'已闭环': 'info',
|
||||||
|
}
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = ref<any[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
const pendingTotal = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const dateRange = ref<string[]>([])
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
status: '全部',
|
||||||
|
keyword: '',
|
||||||
|
company_name: '' as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getDefectiveList({
|
||||||
|
page: query.page,
|
||||||
|
page_size: query.page_size,
|
||||||
|
status: query.status,
|
||||||
|
keyword: query.keyword,
|
||||||
|
start_date: dateRange.value?.[0] || undefined,
|
||||||
|
end_date: dateRange.value?.[1] || undefined,
|
||||||
|
company_name: query.company_name || undefined,
|
||||||
|
})
|
||||||
|
list.value = res.data.list || []
|
||||||
|
total.value = res.data.total || 0
|
||||||
|
pendingTotal.value = res.data.pending_total || 0
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[不良品台账] 查询失败', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
query.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (val: number) => {
|
||||||
|
query.page = val
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFilter = () => {
|
||||||
|
query.page = 1
|
||||||
|
query.status = '全部'
|
||||||
|
query.keyword = ''
|
||||||
|
query.company_name = ''
|
||||||
|
dateRange.value = []
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ★ 修复回库
|
||||||
|
// ============================================================
|
||||||
|
const restockDialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
submitting: false,
|
||||||
|
row: null as any,
|
||||||
|
form: { restock_qty: 0, remark: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const openRestockDialog = (row: any) => {
|
||||||
|
restockDialog.row = row
|
||||||
|
restockDialog.form.restock_qty = Number(row?.remaining_qty || 0)
|
||||||
|
restockDialog.form.remark = ''
|
||||||
|
restockDialog.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetRestockDialog = () => {
|
||||||
|
restockDialog.row = null
|
||||||
|
restockDialog.submitting = false
|
||||||
|
restockDialog.form.restock_qty = 0
|
||||||
|
restockDialog.form.remark = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitRestock = async () => {
|
||||||
|
if (restockDialog.submitting) return // 双保险,防手抖连击
|
||||||
|
|
||||||
|
const max = Number(restockDialog.row?.remaining_qty || 0)
|
||||||
|
const qty = Number(restockDialog.form.restock_qty || 0)
|
||||||
|
if (!qty || qty <= 0) {
|
||||||
|
ElMessage.warning('请填写本次回库数量')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (qty > max) {
|
||||||
|
ElMessage.warning(`回库数量不能超过在管数量 ${max}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restockDialog.submitting = true
|
||||||
|
try {
|
||||||
|
const res = await restockDefective(restockDialog.row.id, {
|
||||||
|
restock_qty: qty,
|
||||||
|
remark: restockDialog.form.remark || undefined,
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.msg || '回库成功')
|
||||||
|
restockDialog.visible = false
|
||||||
|
await fetchData()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[修复回库] 失败', e)
|
||||||
|
} finally {
|
||||||
|
restockDialog.submitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ★ 报废销毁
|
||||||
|
// ============================================================
|
||||||
|
const scrapDialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
submitting: false,
|
||||||
|
row: null as any,
|
||||||
|
form: { scrap_qty: 0, reason: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const openScrapDialog = (row: any) => {
|
||||||
|
scrapDialog.row = row
|
||||||
|
scrapDialog.form.scrap_qty = Number(row?.remaining_qty || 0)
|
||||||
|
scrapDialog.form.reason = ''
|
||||||
|
scrapDialog.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetScrapDialog = () => {
|
||||||
|
scrapDialog.row = null
|
||||||
|
scrapDialog.submitting = false
|
||||||
|
scrapDialog.form.scrap_qty = 0
|
||||||
|
scrapDialog.form.reason = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitScrap = async () => {
|
||||||
|
if (scrapDialog.submitting) return
|
||||||
|
|
||||||
|
const max = Number(scrapDialog.row?.remaining_qty || 0)
|
||||||
|
const qty = Number(scrapDialog.form.scrap_qty || 0)
|
||||||
|
const reason = (scrapDialog.form.reason || '').trim()
|
||||||
|
|
||||||
|
if (!qty || qty <= 0) {
|
||||||
|
ElMessage.warning('请填写本次报废数量')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (qty > max) {
|
||||||
|
ElMessage.warning(`报废数量不能超过在管数量 ${max}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!reason) {
|
||||||
|
ElMessage.warning('请填写报废原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrapDialog.submitting = true
|
||||||
|
try {
|
||||||
|
const res = await scrapDefective(scrapDialog.row.id, {
|
||||||
|
scrap_qty: qty,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
ElMessage.success(res?.msg || '报废成功')
|
||||||
|
scrapDialog.visible = false
|
||||||
|
await fetchData()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[报废销毁] 失败', e)
|
||||||
|
} finally {
|
||||||
|
scrapDialog.submitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.filter-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.dlg-text {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.dlg-strong {
|
||||||
|
color: #E6A23C;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.dlg-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.dlg-danger {
|
||||||
|
color: #F56C6C;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user