电子请购单优化 库存明细页面

This commit is contained in:
jhd
2026-07-01 09:11:15 +08:00
parent 272b29e54a
commit ad650b9a57
6 changed files with 434 additions and 21 deletions

View File

@@ -10,6 +10,7 @@ import com.klp.common.core.validate.AddGroup;
import com.klp.common.core.validate.EditGroup;
import com.klp.common.enums.BusinessType;
import com.klp.common.utils.poi.ExcelUtil;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.klp.erp.domain.bo.ErpPurchaseRequisitionBo;
import com.klp.erp.domain.vo.ErpPurchaseRequisitionVo;
import com.klp.erp.service.IErpPurchaseRequisitionService;
@@ -79,4 +80,28 @@ public class ErpPurchaseRequisitionController extends BaseController {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] reqIds) {
return toAjax(iErpPurchaseRequisitionService.deleteWithValidByIds(Arrays.asList(reqIds), true));
}
/** 提交审批(草稿 → 审批中) */
@Log(title = "请购及采购单", businessType = BusinessType.UPDATE)
@SaCheckPermission("erp:purchaseRequisition:approve")
@PutMapping("/{reqId}/submit")
public R<Void> submitApproval(@NotNull(message = "主键不能为空") @PathVariable Long reqId) {
return toAjax(iErpPurchaseRequisitionService.submitApproval(reqId));
}
/** 审批通过(审批中 → 已通过) */
@Log(title = "请购及采购单", businessType = BusinessType.UPDATE)
@SaCheckPermission("erp:purchaseRequisition:approve")
@PutMapping("/{reqId}/approve")
public R<Void> approve(@NotNull(message = "主键不能为空") @PathVariable Long reqId) {
return toAjax(iErpPurchaseRequisitionService.approve(reqId));
}
/** 驳回(审批中 → 已驳回) */
@Log(title = "请购及采购单", businessType = BusinessType.UPDATE)
@SaCheckPermission("erp:purchaseRequisition:approve")
@PutMapping("/{reqId}/reject")
public R<Void> reject(@NotNull(message = "主键不能为空") @PathVariable Long reqId) {
return toAjax(iErpPurchaseRequisitionService.reject(reqId));
}
}

View File

@@ -33,4 +33,13 @@ public interface IErpPurchaseRequisitionService {
/** 校验并批量删除请购单 */
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/** 提交审批(草稿 → 审批中) */
Boolean submitApproval(Long reqId);
/** 审批通过(审批中 → 已通过) */
Boolean approve(Long reqId);
/** 驳回(审批中 → 已驳回) */
Boolean reject(Long reqId);
}

View File

@@ -83,22 +83,6 @@ public class ErpPurchaseRequisitionServiceImpl implements IErpPurchaseRequisitio
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(ErpPurchaseRequisitionBo bo) {
ErpPurchaseRequisition update = baseMapper.selectById(bo.getReqId());
if (update == null) {
return false;
}
BeanUtil.copyProperties(bo, update, "reqId", "createBy", "createTime");
baseMapper.updateById(update);
// 覆盖式重写明细
itemMapper.delete(Wrappers.lambdaQuery(ErpPurchaseRequisitionItem.class)
.eq(ErpPurchaseRequisitionItem::getReqId, bo.getReqId()));
saveItems(bo.getReqId(), bo.getItems());
return true;
}
private void saveItems(Long reqId, List<ErpPurchaseRequisitionItemBo> items) {
if (items == null || items.isEmpty()) {
return;
@@ -120,10 +104,69 @@ public class ErpPurchaseRequisitionServiceImpl implements IErpPurchaseRequisitio
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
for (Long reqId : ids) {
ErpPurchaseRequisition entity = baseMapper.selectById(reqId);
if (entity != null && !"0".equals(entity.getFormStatus())) {
throw new RuntimeException("仅草稿状态的请购单可删除");
}
itemMapper.delete(Wrappers.lambdaQuery(ErpPurchaseRequisitionItem.class)
.eq(ErpPurchaseRequisitionItem::getReqId, reqId));
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(ErpPurchaseRequisitionBo bo) {
ErpPurchaseRequisition update = baseMapper.selectById(bo.getReqId());
if (update == null) {
return false;
}
if (!"0".equals(update.getFormStatus())) {
throw new RuntimeException("仅草稿状态的请购单可修改");
}
BeanUtil.copyProperties(bo, update, "reqId", "createBy", "createTime");
baseMapper.updateById(update);
// 覆盖式重写明细
itemMapper.delete(Wrappers.lambdaQuery(ErpPurchaseRequisitionItem.class)
.eq(ErpPurchaseRequisitionItem::getReqId, bo.getReqId()));
saveItems(bo.getReqId(), bo.getItems());
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean submitApproval(Long reqId) {
ErpPurchaseRequisition entity = baseMapper.selectById(reqId);
if (entity == null) return false;
if (!"0".equals(entity.getFormStatus())) {
throw new RuntimeException("仅草稿状态的请购单可提交审批");
}
entity.setFormStatus("1");
return baseMapper.updateById(entity) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean approve(Long reqId) {
ErpPurchaseRequisition entity = baseMapper.selectById(reqId);
if (entity == null) return false;
if (!"1".equals(entity.getFormStatus())) {
throw new RuntimeException("仅审批中状态的请购单可通过");
}
entity.setFormStatus("2");
return baseMapper.updateById(entity) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean reject(Long reqId) {
ErpPurchaseRequisition entity = baseMapper.selectById(reqId);
if (entity == null) return false;
if (!"1".equals(entity.getFormStatus())) {
throw new RuntimeException("仅审批中状态的请购单可驳回");
}
entity.setFormStatus("5");
return baseMapper.updateById(entity) > 0;
}
}

View File

@@ -35,6 +35,30 @@ export function updatePurchaseRequisition(data) {
})
}
// 提交审批
export function submitApproval(reqId) {
return request({
url: '/erp/purchaseRequisition/' + reqId + '/submit',
method: 'put'
})
}
// 审批通过
export function approvePurchase(reqId) {
return request({
url: '/erp/purchaseRequisition/' + reqId + '/approve',
method: 'put'
})
}
// 驳回
export function rejectPurchase(reqId) {
return request({
url: '/erp/purchaseRequisition/' + reqId + '/reject',
method: 'put'
})
}
// 删除请购单
export function delPurchaseRequisition(reqIds) {
return request({

View File

@@ -59,11 +59,15 @@
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)" v-hasPermi="['erp:purchaseRequisition:query']">查看</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['erp:purchaseRequisition:edit']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['erp:purchaseRequisition:remove']">删除</el-button>
<div style="display:flex;flex-wrap:wrap;">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)" v-hasPermi="['erp:purchaseRequisition:query']" style="width:50%;margin:0;text-align:center">查看</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-if="scope.row.formStatus === '0'" v-hasPermi="['erp:purchaseRequisition:edit']" style="width:50%;margin:0;text-align:center">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-if="scope.row.formStatus === '0'" v-hasPermi="['erp:purchaseRequisition:remove']" style="width:50%;margin:0;text-align:center">删除</el-button>
<el-button size="mini" type="text" icon="el-icon-upload2" @click="handleSubmitOne(scope.row)" v-if="scope.row.formStatus === '0'" v-hasPermi="['erp:purchaseRequisition:approve']" style="width:50%;margin:0;text-align:center">提交</el-button>
<el-button size="mini" type="text" icon="el-icon-circle-check" style="color:#67c23a;width:50%;margin:0;text-align:center" @click="openApproval(scope.row)" v-if="scope.row.formStatus === '1'" v-hasPermi="['erp:purchaseRequisition:approve']">审批</el-button>
</div>
</template>
</el-table-column>
</KLPTable>
@@ -355,10 +359,42 @@
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button icon="el-icon-circle-check" style="color:#67c23a;float:left" @click="openApproval(viewForm)" v-if="viewForm.formStatus === '1'" v-hasPermi="['erp:purchaseRequisition:approve']">审批</el-button>
<el-button type="primary" icon="el-icon-download" :loading="printing" @click="exportPdf">导出PDF</el-button>
</div>
</el-dialog>
<!-- 审批弹窗 -->
<el-dialog title="审批请购单" :visible.sync="approvalOpen" width="560px" append-to-body :close-on-click-modal="false">
<div style="padding:0 8px">
<el-form size="small" label-width="90px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="请购部门">{{ approvalReq.reqDept || '—' }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="品名">{{ approvalReq.itemName || '—' }}</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="规格">{{ approvalReq.specification || '—' }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="请购量">{{ approvalReq.quantity || '—' }}</el-form-item>
</el-col>
</el-row>
<el-form-item label="审批意见">
<el-input v-model="approvalRemark" type="textarea" :rows="3" placeholder="请输入审批意见(可选)" />
</el-form-item>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="doReject" type="danger" icon="el-icon-close" :loading="approvalLoading"> </el-button>
<el-button @click="doApprove" type="primary" icon="el-icon-check" :loading="approvalLoading"> </el-button>
</div>
</el-dialog>
<!-- A4 打印模板屏幕外渲染 html2canvas 截图 -->
<div class="pr-print-wrap" v-show="printing">
<div class="pr-print" ref="printTemplate">
@@ -515,7 +551,10 @@ import {
getPurchaseRequisition,
addPurchaseRequisition,
updatePurchaseRequisition,
delPurchaseRequisition
delPurchaseRequisition,
submitApproval,
approvePurchase,
rejectPurchase
} from '@/api/erp/purchaseRequisition'
export default {
@@ -545,6 +584,11 @@ export default {
viewForm: {},
inspectionChecks: [],
printData: {},
// 审批
approvalOpen: false,
approvalReq: {},
approvalRemark: '',
approvalLoading: false,
rules: {}
}
},
@@ -630,6 +674,39 @@ export default {
this.viewForm = res.data || {}
})
},
// 提交审批(单个操作列)
handleSubmitOne(row) {
this.$modal.confirm('确认提交该请购单进行审批?').then(() => {
return submitApproval(row.reqId)
}).then(() => {
this.$modal.msgSuccess('已提交审批')
this.getList()
}).catch(() => {})
},
// 打开审批弹窗
openApproval(row) {
this.approvalReq = { ...row }
this.approvalRemark = ''
this.approvalOpen = true
},
// 审批通过
doApprove() {
this.approvalLoading = true
approvePurchase(this.approvalReq.reqId).then(() => {
this.$modal.msgSuccess('审批通过')
this.approvalOpen = false
this.getList()
}).finally(() => { this.approvalLoading = false })
},
// 驳回
doReject() {
this.approvalLoading = true
rejectPurchase(this.approvalReq.reqId).then(() => {
this.$modal.msgSuccess('已驳回')
this.approvalOpen = false
this.getList()
}).finally(() => { this.approvalLoading = false })
},
cancel() {
this.open = false
},

View File

@@ -0,0 +1,235 @@
<template>
<div class="app-container" v-loading="loading">
<!-- 标题 -->
<h2 style="text-align: center; margin-bottom: 16px;">{{ title }}</h2>
<!-- 空数据 -->
<el-empty v-if="!loading && tableData.length === 0" description="暂无数据" />
<!-- 数据表格 -->
<el-table
v-if="tableData.length > 0"
:data="tableData"
border
style="width: 100%"
:header-cell-style="{ background: '#f2f2f2', fontWeight: 600, textAlign: 'center' }"
:cell-style="cellStyle"
size="small"
>
<el-table-column label="业务员" prop="dimension" min-width="100" align="center" />
<el-table-column v-for="cat in CATEGORIES" :key="cat.key" :label="cat.label" align="center">
<el-table-column :label="'A/B'" :prop="cat.key + '_AB'" width="85" align="center">
<template slot-scope="{ row }">{{ formatNum(row[cat.key + '_AB']) }}</template>
</el-table-column>
<el-table-column :label="'C/D/O'" :prop="cat.key + '_CDO'" width="85" align="center">
<template slot-scope="{ row }">{{ formatNum(row[cat.key + '_CDO']) }}</template>
</el-table-column>
</el-table-column>
<el-table-column label="合计" prop="total" width="100" align="center">
<template slot-scope="{ row }">{{ formatNum(row.total) }}</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { listCoilWithIds } from "@/api/wms/coil";
const CATEGORIES = [
{ key: '冷硬钢卷', label: '冷硬钢卷' },
{ key: '冷轧钢卷', label: '冷轧钢卷' },
{ key: '镀锌钢卷', label: '镀锌钢卷' },
{ key: '镀锌管材', label: '镀锌管材' },
{ key: '花纹板', label: '花纹板' },
{ key: '镀铬', label: '镀铬' },
]
/**
* 品质等级 → 分组映射
* A/B → 'AB', C/D/O → 'CDO', 其他 → null
*/
function getQualityGroup(status) {
if (!status) return null
const s = String(status).toUpperCase().trim()
if (['A', 'B'].includes(s)) return 'AB'
if (['C', 'D', 'O'].includes(s)) return 'CDO'
return null
}
export default {
name: "StockDetail",
dicts: ['wip_pack_saleman'],
data() {
return {
loading: false,
rawList: [],
}
},
computed: {
CATEGORIES: () => CATEGORIES,
title() {
const now = new Date()
return `${now.getMonth()+1}.${now.getDate()}.${String(now.getFullYear()).slice(-2)} 库存明细`
},
tableData() {
if (!this.rawList.length) return []
// ========== 第一层:分类聚合 ==========
// orderData: { [saleName]: { [catKey]: { AB: 0, CDO: 0 } } }
// spotData: { [catKey]: { AB: 0, CDO: 0 } }
const orderData = {}
const spotData = {}
let unmatchedCount = 0
this.rawList.forEach(item => {
// 匹配钢材品类
const cat = CATEGORIES.find(c => item.itemName && item.itemName.includes(c.key))
if (!cat) { unmatchedCount++; return }
// 品质分组
const group = getQualityGroup(item.qualityStatus)
if (!group) { unmatchedCount++; return }
const weight = Number(item.netWeight) || 0
if (Number(item.isRelatedToOrder) === 1 && item.saleName) {
// 订单相关 → 归属业务员
if (!orderData[item.saleName]) orderData[item.saleName] = {}
if (!orderData[item.saleName][cat.key]) orderData[item.saleName][cat.key] = { AB: 0, CDO: 0 }
orderData[item.saleName][cat.key][group] += weight
} else {
// 非订单相关 → 现货
if (!spotData[cat.key]) spotData[cat.key] = { AB: 0, CDO: 0 }
spotData[cat.key][group] += weight
}
})
if (unmatchedCount > 0) {
console.log(`[库存明细] 未匹配品类的钢卷数: ${unmatchedCount}`)
}
// ========== 第二层:组装行数据 ==========
// 工具函数:创建空行
const createRow = (dimension, rowType, remark = '') => {
const row = { dimension, rowType, remark, total: 0 }
CATEGORIES.forEach(c => { row[c.key + '_AB'] = 0; row[c.key + '_CDO'] = 0 })
return row
}
// 2.1 业务员行(按字典 wip_pack_saleman 顺序)
const dictItems = this.dict.type.wip_pack_saleman || []
const personRows = dictItems.map(dictItem => {
const row = createRow(dictItem.label, 'person')
const personData = orderData[dictItem.value] || {}
CATEGORIES.forEach(cat => {
const vals = personData[cat.key] || { AB: 0, CDO: 0 }
row[cat.key + '_AB'] = vals.AB
row[cat.key + '_CDO'] = vals.CDO
row.total += vals.AB + vals.CDO
})
return row
})
// 2.2 订单小计
const makeSubtotal = () => {
const row = createRow('订单小计', 'summary')
CATEGORIES.forEach(cat => {
let ab = 0, cdo = 0
personRows.forEach(r => {
ab += r[cat.key + '_AB'] || 0
cdo += r[cat.key + '_CDO'] || 0
})
row[cat.key + '_AB'] = ab
row[cat.key + '_CDO'] = cdo
row.total += ab + cdo
})
return row
}
// 2.3 现货
const makeSpot = () => {
const row = createRow('现货', 'summary')
CATEGORIES.forEach(cat => {
const vals = spotData[cat.key] || { AB: 0, CDO: 0 }
row[cat.key + '_AB'] = vals.AB
row[cat.key + '_CDO'] = vals.CDO
row.total += vals.AB + vals.CDO
})
return row
}
// 2.4 合计 = 订单小计 + 现货
const makeTotal = () => {
const row = createRow('合计', 'summary')
CATEGORIES.forEach(cat => {
const ab = (orderSubtotal[cat.key + '_AB'] || 0) + (spotRow[cat.key + '_AB'] || 0)
const cdo = (orderSubtotal[cat.key + '_CDO'] || 0) + (spotRow[cat.key + '_CDO'] || 0)
row[cat.key + '_AB'] = ab
row[cat.key + '_CDO'] = cdo
row.total += ab + cdo
})
return row
}
// 2.5 各产品小计
const makeProductSummary = () => {
const row = createRow('各产品小计', 'summary')
CATEGORIES.forEach(cat => {
const merged = (totalRow[cat.key + '_AB'] || 0) + (totalRow[cat.key + '_CDO'] || 0)
row[cat.key + '_AB'] = merged
row[cat.key + '_CDO'] = 0
row.total += merged
})
return row
}
const orderSubtotal = makeSubtotal()
const spotRow = makeSpot()
const totalRow = makeTotal()
return [
...personRows,
orderSubtotal,
spotRow,
totalRow,
makeProductSummary(),
]
},
},
methods: {
formatNum(val) {
if (val === null || val === undefined || isNaN(val)) return '0.00'
return Number(val).toFixed(2)
},
cellStyle({ row }) {
return row.rowType === 'summary' ? { backgroundColor: '#FFFACD' } : {}
},
getList() {
this.loading = true
this.rawList = []
listCoilWithIds({
status: 0,
dataType: 1,
pageSize: 99999,
pageNum: 1,
}).then(res => {
this.rawList = res.rows || []
this.loading = false
}).catch(() => {
this.loading = false
})
},
},
mounted() {
this.getList()
},
}
</script>
<style scoped>
</style>