采购需求添加绑定物料
This commit is contained in:
@@ -45,6 +45,10 @@ public class OaRequirements extends BaseEntity {
|
|||||||
* 挂接项目 ID,可选
|
* 挂接项目 ID,可选
|
||||||
*/
|
*/
|
||||||
private Long projectId;
|
private Long projectId;
|
||||||
|
/**
|
||||||
|
* 关联物料 ID CSV -> sys_oa_warehouse.id,可选
|
||||||
|
*/
|
||||||
|
private String materialIds;
|
||||||
/**
|
/**
|
||||||
* 需求描述
|
* 需求描述
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ public class OaRequirementsBo extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private Long projectId;
|
private Long projectId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联物料 ID CSV,多个物料用逗号分隔
|
||||||
|
*/
|
||||||
|
private String materialIds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 需求描述
|
* 需求描述
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -89,6 +89,22 @@ public class OaRequirementsVo extends BaseEntity {
|
|||||||
private String ownerNickName;
|
private String ownerNickName;
|
||||||
private String projectName;
|
private String projectName;
|
||||||
|
|
||||||
|
/** 关联物料 ID CSV */
|
||||||
|
private String materialIds;
|
||||||
|
/** 关联物料明细(service 层 enrich,列表/详情都返回) */
|
||||||
|
private java.util.List<MaterialItem> materials;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MaterialItem implements java.io.Serializable {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String model;
|
||||||
|
private String unit;
|
||||||
|
private Long inventory;
|
||||||
|
private String brand;
|
||||||
|
private String specifications;
|
||||||
|
}
|
||||||
|
|
||||||
/** 附件文件列表(已联查 sys_oss,每项形如 "<ossId>|<originalName>|<url>",逗号分隔) */
|
/** 附件文件列表(已联查 sys_oss,每项形如 "<ossId>|<originalName>|<url>",逗号分隔) */
|
||||||
private String accessoryFiles;
|
private String accessoryFiles;
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,19 @@ import org.springframework.stereotype.Service;
|
|||||||
import com.ruoyi.oa.domain.bo.OaRequirementsBo;
|
import com.ruoyi.oa.domain.bo.OaRequirementsBo;
|
||||||
import com.ruoyi.oa.domain.vo.OaRequirementsVo;
|
import com.ruoyi.oa.domain.vo.OaRequirementsVo;
|
||||||
import com.ruoyi.oa.domain.OaRequirements;
|
import com.ruoyi.oa.domain.OaRequirements;
|
||||||
|
import com.ruoyi.oa.domain.SysOaWarehouse;
|
||||||
import com.ruoyi.oa.mapper.OaRequirementsMapper;
|
import com.ruoyi.oa.mapper.OaRequirementsMapper;
|
||||||
|
import com.ruoyi.oa.mapper.SysOaWarehouseMapper;
|
||||||
import com.ruoyi.oa.service.IOaRequirementsService;
|
import com.ruoyi.oa.service.IOaRequirementsService;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OA 需求Service业务层处理
|
* OA 需求Service业务层处理
|
||||||
@@ -37,12 +43,63 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
|||||||
|
|
||||||
private final OaWarehouseAuditService auditService;
|
private final OaWarehouseAuditService auditService;
|
||||||
|
|
||||||
|
private final SysOaWarehouseMapper warehouseMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询OA 需求
|
* 查询OA 需求
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public OaRequirementsVo queryById(Long requirementId){
|
public OaRequirementsVo queryById(Long requirementId){
|
||||||
return baseMapper.selectVoById(requirementId);
|
OaRequirementsVo vo = baseMapper.selectVoById(requirementId);
|
||||||
|
enrichMaterials(Collections.singletonList(vo));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据 material_ids(CSV)批量补全物料明细到 VO.materials */
|
||||||
|
private void enrichMaterials(List<OaRequirementsVo> list) {
|
||||||
|
if (list == null || list.isEmpty()) return;
|
||||||
|
Set<Long> ids = new HashSet<>();
|
||||||
|
for (OaRequirementsVo v : list) {
|
||||||
|
if (v == null) continue;
|
||||||
|
for (Long id : parseCsv(v.getMaterialIds())) ids.add(id);
|
||||||
|
}
|
||||||
|
if (ids.isEmpty()) return;
|
||||||
|
List<SysOaWarehouse> wList = warehouseMapper.selectList(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysOaWarehouse>()
|
||||||
|
.in("id", ids));
|
||||||
|
Map<Long, SysOaWarehouse> wMap = wList.stream()
|
||||||
|
.collect(Collectors.toMap(SysOaWarehouse::getId, w -> w, (a, b) -> a));
|
||||||
|
for (OaRequirementsVo v : list) {
|
||||||
|
if (v == null) continue;
|
||||||
|
List<Long> mids = parseCsv(v.getMaterialIds());
|
||||||
|
if (mids.isEmpty()) continue;
|
||||||
|
List<OaRequirementsVo.MaterialItem> items = new ArrayList<>();
|
||||||
|
for (Long id : mids) {
|
||||||
|
SysOaWarehouse w = wMap.get(id);
|
||||||
|
if (w == null) continue;
|
||||||
|
OaRequirementsVo.MaterialItem it = new OaRequirementsVo.MaterialItem();
|
||||||
|
it.setId(w.getId());
|
||||||
|
it.setName(w.getName());
|
||||||
|
it.setModel(w.getModel());
|
||||||
|
it.setUnit(w.getUnit());
|
||||||
|
it.setInventory(w.getInventory());
|
||||||
|
it.setBrand(w.getBrand());
|
||||||
|
it.setSpecifications(w.getSpecifications());
|
||||||
|
items.add(it);
|
||||||
|
}
|
||||||
|
v.setMaterials(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> parseCsv(String csv) {
|
||||||
|
if (csv == null || csv.isEmpty()) return Collections.emptyList();
|
||||||
|
List<Long> r = new ArrayList<>();
|
||||||
|
for (String s : csv.split(",")) {
|
||||||
|
String t = s.trim();
|
||||||
|
if (t.isEmpty()) continue;
|
||||||
|
try { r.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +109,7 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
|||||||
public TableDataInfo<OaRequirementsVo> queryPageList(OaRequirementsBo bo, PageQuery pageQuery) {
|
public TableDataInfo<OaRequirementsVo> queryPageList(OaRequirementsBo bo, PageQuery pageQuery) {
|
||||||
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
|
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
|
||||||
Page<OaRequirementsVo> result = baseMapper.selectVoListPage(pageQuery.build(), lqw);
|
Page<OaRequirementsVo> result = baseMapper.selectVoListPage(pageQuery.build(), lqw);
|
||||||
|
enrichMaterials(result.getRecords());
|
||||||
return TableDataInfo.build(result);
|
return TableDataInfo.build(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +119,9 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
|
|||||||
@Override
|
@Override
|
||||||
public List<OaRequirementsVo> queryList(OaRequirementsBo bo) {
|
public List<OaRequirementsVo> queryList(OaRequirementsBo bo) {
|
||||||
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
|
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
|
||||||
return baseMapper.selectVoListForExport(lqw);
|
List<OaRequirementsVo> list = baseMapper.selectVoListForExport(lqw);
|
||||||
|
enrichMaterials(list);
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryWrapper<OaRequirements> buildQueryWrapper(OaRequirementsBo bo) {
|
private QueryWrapper<OaRequirements> buildQueryWrapper(OaRequirementsBo bo) {
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ public class SysOaWarehouseServiceImpl implements ISysOaWarehouseService {
|
|||||||
.like("sow.brand", bo.getName())
|
.like("sow.brand", bo.getName())
|
||||||
.or()
|
.or()
|
||||||
.like("sow.model", bo.getName())
|
.like("sow.model", bo.getName())
|
||||||
|
.or()
|
||||||
|
.like("sow.specifications", bo.getName())
|
||||||
)
|
)
|
||||||
.eq(StringUtils.isNotBlank(bo.getModel()), "sow.model", bo.getModel())
|
.eq(StringUtils.isNotBlank(bo.getModel()), "sow.model", bo.getModel())
|
||||||
.eq(StringUtils.isNotBlank(bo.getBrand()), "sow.brand", bo.getBrand());
|
.eq(StringUtils.isNotBlank(bo.getBrand()), "sow.brand", bo.getBrand());
|
||||||
|
|||||||
@@ -66,40 +66,25 @@
|
|||||||
|
|
||||||
<!-- 新增提示组件 -->
|
<!-- 新增提示组件 -->
|
||||||
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
|
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
|
||||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange"
|
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
|
||||||
@expand-change="onExpandChange">
|
|
||||||
<el-table-column type="expand" width="36">
|
|
||||||
<template slot-scope="props">
|
|
||||||
<div style="padding: 8px 24px; background:#fafafa;">
|
|
||||||
<div style="font-weight:600; margin-bottom:6px;">
|
|
||||||
已入库批次({{ (batchMap[props.row.requirementId] || []).length }} 批)
|
|
||||||
</div>
|
|
||||||
<el-table v-loading="batchLoading[props.row.requirementId]"
|
|
||||||
:data="batchMap[props.row.requirementId] || []" size="mini" stripe>
|
|
||||||
<el-table-column label="入库时间" prop="signTime" width="160">
|
|
||||||
<template slot-scope="s">{{ parseTime(s.row.signTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="入库单" prop="masterNum" width="160" />
|
|
||||||
<el-table-column label="入库人" prop="signUser" width="100" />
|
|
||||||
<el-table-column label="物料概览" prop="summary" min-width="240" show-overflow-tooltip />
|
|
||||||
<el-table-column label="总数量" prop="totalQty" width="80" align="right" />
|
|
||||||
<el-table-column label="总金额" width="110" align="right">
|
|
||||||
<template slot-scope="s">¥{{ Number(s.row.totalAmount || 0).toFixed(2) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div v-if="(batchMap[props.row.requirementId] || []).length === 0
|
|
||||||
&& !batchLoading[props.row.requirementId]"
|
|
||||||
style="text-align:center; color:#909399; padding:12px 0;">
|
|
||||||
暂无入库批次
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
<el-table-column label="需求标题" align="center" prop="title" min-width="160" show-overflow-tooltip />
|
<el-table-column label="需求标题" align="center" prop="title" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column label="需求方" align="center" prop="requesterNickName" width="100" show-overflow-tooltip />
|
<el-table-column label="需求方" align="center" prop="requesterNickName" width="100" show-overflow-tooltip />
|
||||||
<el-table-column label="负责人" align="center" prop="ownerNickName" width="100" show-overflow-tooltip />
|
<el-table-column label="负责人" align="center" prop="ownerNickName" width="100" show-overflow-tooltip />
|
||||||
<el-table-column label="关联项目" align="center" prop="projectName" min-width="160" show-overflow-tooltip />
|
<el-table-column label="关联项目" align="center" prop="projectName" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="采购物料" align="left" min-width="240">
|
||||||
|
<template slot-scope="{ row }">
|
||||||
|
<template v-if="row.materials && row.materials.length">
|
||||||
|
<div v-for="m in row.materials" :key="m.id" class="mat-row">
|
||||||
|
<span class="mat-name">{{ m.name }}<span v-if="m.model" class="mat-model"> / {{ m.model }}</span></span>
|
||||||
|
<span class="mat-stock" :class="{ low: (m.inventory||0) <= 0 }">
|
||||||
|
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else style="color:#c0c4cc;">未关联</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="需求描述" align="center" prop="description" min-width="200" show-overflow-tooltip>
|
<el-table-column label="需求描述" align="center" prop="description" min-width="200" show-overflow-tooltip>
|
||||||
<template slot-scope="{ row }">
|
<template slot-scope="{ row }">
|
||||||
<span v-if="row.description" class="copyable-text" @click="copyText(row.description)"
|
<span v-if="row.description" class="copyable-text" @click="copyText(row.description)"
|
||||||
@@ -153,9 +138,6 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" icon="el-icon-truck" style="color:#409eff"
|
|
||||||
v-if="scope.row.status !== 2 && scope.row.status !== 3"
|
|
||||||
@click="handleGoToInbound(scope.row)">执行入库</el-button>
|
|
||||||
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
|
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
|
||||||
v-if="scope.row.status === 1">完成</el-button>
|
v-if="scope.row.status === 1">完成</el-button>
|
||||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
|
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
|
||||||
@@ -184,6 +166,25 @@
|
|||||||
<el-form-item label="关联项目" prop="projectId">
|
<el-form-item label="关联项目" prop="projectId">
|
||||||
<project-select v-model="form.projectId" style="width: 100%" />
|
<project-select v-model="form.projectId" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="采购物料" prop="materialIdArr">
|
||||||
|
<div class="mat-trigger">
|
||||||
|
<el-button size="mini" icon="el-icon-search" @click="openMaterialPicker">
|
||||||
|
选择物料{{ selectedMaterials.length ? `(已选 ${selectedMaterials.length})` : '' }}
|
||||||
|
</el-button>
|
||||||
|
<span v-if="!selectedMaterials.length" class="mat-trigger-hint">可多选;留空也可保存,后续再关联</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedMaterials.length" class="mat-stock-panel">
|
||||||
|
<div v-for="m in selectedMaterials" :key="m.id" class="mat-stock-row">
|
||||||
|
<span class="mat-stock-name">{{ m.name }}<span v-if="m.model" style="color:#909399;"> / {{ m.model }}</span></span>
|
||||||
|
<span class="mat-stock-tag" :class="{ low: (m.inventory||0) <= 0 }">
|
||||||
|
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
|
||||||
|
</span>
|
||||||
|
<span v-if="m.brand" class="mat-stock-meta">品牌:{{ m.brand }}</span>
|
||||||
|
<span v-if="m.specifications" class="mat-stock-meta">规格:{{ m.specifications }}</span>
|
||||||
|
<i class="el-icon-close mat-remove" title="移除" @click="removeSelectedMaterial(m.id)"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="需求描述" prop="description">
|
<el-form-item label="需求描述" prop="description">
|
||||||
<el-input v-model="form.description" type="textarea" placeholder="请输入需求描述" />
|
<el-input v-model="form.description" type="textarea" placeholder="请输入需求描述" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -205,6 +206,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 物料选择器 -->
|
||||||
|
<el-dialog title="选择采购物料" :visible.sync="materialPickerOpen" width="780px"
|
||||||
|
append-to-body :close-on-click-modal="false" @open="onPickerOpen">
|
||||||
|
<div class="picker-toolbar">
|
||||||
|
<el-input v-model="picker.kw" placeholder="搜索 名称 / 型号 / 品牌 / 规格"
|
||||||
|
size="mini" clearable prefix-icon="el-icon-search"
|
||||||
|
style="width: 320px;" @input="onPickerSearch" @clear="onPickerSearch" />
|
||||||
|
<el-button size="mini" type="primary" plain icon="el-icon-plus"
|
||||||
|
style="margin-left:8px;" @click="openInlineNew">未找到?新增物料</el-button>
|
||||||
|
<span class="picker-tip" v-if="picker.tempSelected.length">
|
||||||
|
已勾选 <b>{{ picker.tempSelected.length }}</b> 项
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内嵌新增物料表单 -->
|
||||||
|
<el-card v-if="newMat.show" shadow="never" class="new-mat-card">
|
||||||
|
<div slot="header" class="new-mat-header">
|
||||||
|
<span><i class="el-icon-plus" /> 新增物料到库存</span>
|
||||||
|
<el-button type="text" size="mini" icon="el-icon-close" @click="newMat.show = false">收起</el-button>
|
||||||
|
</div>
|
||||||
|
<el-form :model="newMat" :rules="newMatRules" ref="newMatForm" size="mini"
|
||||||
|
label-width="70px" :inline="true">
|
||||||
|
<el-form-item label="名称" prop="name" required>
|
||||||
|
<el-input v-model="newMat.name" placeholder="物料名称" style="width:180px;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="型号" prop="model">
|
||||||
|
<el-input v-model="newMat.model" placeholder="可选" style="width:140px;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="规格" prop="specifications">
|
||||||
|
<el-input v-model="newMat.specifications" placeholder="可选" style="width:140px;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="品牌" prop="brand">
|
||||||
|
<el-input v-model="newMat.brand" placeholder="可选" style="width:120px;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="单位" prop="unit">
|
||||||
|
<el-input v-model="newMat.unit" placeholder="个/箱/kg" style="width:100px;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="初始库存" prop="inventory" required>
|
||||||
|
<el-input-number v-model="newMat.inventory" :min="0" :step="1" size="mini" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button size="mini" type="primary" :loading="newMat.saving" @click="submitNewMat">保存并选中</el-button>
|
||||||
|
<el-button size="mini" @click="newMat.show = false">取消</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-table :data="picker.list" v-loading="picker.loading" size="mini"
|
||||||
|
ref="pickerTable" border highlight-current-row max-height="380"
|
||||||
|
row-key="id" :row-class-name="pickerRowClass"
|
||||||
|
@row-click="onPickerRowClick"
|
||||||
|
@selection-change="onPickerSelectionChange">
|
||||||
|
<el-table-column type="selection" width="42" reserve-selection />
|
||||||
|
<el-table-column label="名称" prop="name" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="型号" prop="model" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="规格" prop="specifications" width="110" show-overflow-tooltip />
|
||||||
|
<el-table-column label="品牌" prop="brand" width="100" show-overflow-tooltip />
|
||||||
|
<el-table-column label="单位" prop="unit" width="60" align="center" />
|
||||||
|
<el-table-column label="库存" width="90" align="right">
|
||||||
|
<template slot-scope="s">
|
||||||
|
<span :style="{color: (s.row.inventory||0) <= 0 ? '#f56c6c' : '#67c23a', fontWeight:600}">
|
||||||
|
{{ s.row.inventory == null ? 0 : s.row.inventory }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="picker-pager" v-if="picker.total > picker.pageSize">
|
||||||
|
<el-pagination small background layout="prev, pager, next, total"
|
||||||
|
:total="picker.total" :page-size="picker.pageSize" :current-page.sync="picker.pageNum"
|
||||||
|
@current-change="loadPickerList" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<el-button @click="materialPickerOpen = false">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmPicker">确定({{ picker.tempSelected.length }})</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 需求详情对话框 -->
|
<!-- 需求详情对话框 -->
|
||||||
<el-dialog title="需求详情" :visible.sync="detailDialog" width="600px" append-to-body>
|
<el-dialog title="需求详情" :visible.sync="detailDialog" width="600px" append-to-body>
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
@@ -233,8 +312,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { addRequirements, delRequirements, getRequirements, getRequirementBatches, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||||
import { listUser } from "@/api/system/user";
|
import { listUser } from "@/api/system/user";
|
||||||
|
import { listOaWarehouse, getOaWarehouse, addOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
|
||||||
import FilePreview from '@/components/FilePreview';
|
import FilePreview from '@/components/FilePreview';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/FileUpload';
|
||||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||||
@@ -244,9 +324,35 @@ export default {
|
|||||||
components: { FileUpload, FilePreview, ProjectSelect },
|
components: { FileUpload, FilePreview, ProjectSelect },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
// 入库批次(按 requirementId 缓存)
|
// 当前已选物料明细(用于库存展示)
|
||||||
batchMap: {},
|
selectedMaterials: [],
|
||||||
batchLoading: {},
|
// 物料选择器
|
||||||
|
materialPickerOpen: false,
|
||||||
|
picker: {
|
||||||
|
kw: '',
|
||||||
|
loading: false,
|
||||||
|
list: [],
|
||||||
|
total: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
tempSelected: [], // 选择器内部勾选项(含完整物料对象)
|
||||||
|
searchTimer: null,
|
||||||
|
},
|
||||||
|
// 新增物料内嵌表单
|
||||||
|
newMat: {
|
||||||
|
show: false,
|
||||||
|
saving: false,
|
||||||
|
name: '',
|
||||||
|
model: '',
|
||||||
|
specifications: '',
|
||||||
|
brand: '',
|
||||||
|
unit: '',
|
||||||
|
inventory: 0,
|
||||||
|
},
|
||||||
|
newMatRules: {
|
||||||
|
name: [{ required: true, message: '物料名称不能为空', trigger: 'blur' }],
|
||||||
|
inventory: [{ required: true, message: '初始库存不能为空', trigger: 'change' }],
|
||||||
|
},
|
||||||
// 按钮loading
|
// 按钮loading
|
||||||
buttonLoading: false,
|
buttonLoading: false,
|
||||||
// 遮罩层
|
// 遮罩层
|
||||||
@@ -324,11 +430,120 @@ export default {
|
|||||||
this.refreshStat();
|
this.refreshStat();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 跳到入库明细页面,并预填该采购需求
|
// ====== 物料选择器 ======
|
||||||
handleGoToInbound (row) {
|
openMaterialPicker () {
|
||||||
this.$router.push({
|
this.picker.kw = ''
|
||||||
path: '/step/in',
|
this.picker.pageNum = 1
|
||||||
query: { requirementId: String(row.requirementId), requirementTitle: row.title }
|
// 当前已选项作为初始勾选
|
||||||
|
this.picker.tempSelected = this.selectedMaterials.slice()
|
||||||
|
this.newMat.show = false
|
||||||
|
this.materialPickerOpen = true
|
||||||
|
},
|
||||||
|
onPickerOpen () {
|
||||||
|
this.loadPickerList().then(() => this.syncPickerSelectionUI())
|
||||||
|
},
|
||||||
|
loadPickerList () {
|
||||||
|
this.picker.loading = true
|
||||||
|
const params = {
|
||||||
|
pageNum: this.picker.pageNum,
|
||||||
|
pageSize: this.picker.pageSize,
|
||||||
|
}
|
||||||
|
if (this.picker.kw && this.picker.kw.trim()) params.name = this.picker.kw.trim()
|
||||||
|
return listOaWarehouse(params).then(res => {
|
||||||
|
this.picker.list = res.rows || []
|
||||||
|
this.picker.total = res.total || 0
|
||||||
|
this.$nextTick(() => this.syncPickerSelectionUI())
|
||||||
|
}).finally(() => { this.picker.loading = false })
|
||||||
|
},
|
||||||
|
onPickerSearch () {
|
||||||
|
clearTimeout(this.picker.searchTimer)
|
||||||
|
this.picker.searchTimer = setTimeout(() => {
|
||||||
|
this.picker.pageNum = 1
|
||||||
|
this.loadPickerList()
|
||||||
|
}, 250)
|
||||||
|
},
|
||||||
|
// 表格分页/刷新后把已勾选项重新打勾
|
||||||
|
syncPickerSelectionUI () {
|
||||||
|
const tbl = this.$refs.pickerTable
|
||||||
|
if (!tbl) return
|
||||||
|
tbl.clearSelection()
|
||||||
|
const ids = new Set(this.picker.tempSelected.map(m => m.id))
|
||||||
|
for (const row of this.picker.list) {
|
||||||
|
if (ids.has(row.id)) tbl.toggleRowSelection(row, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPickerSelectionChange (rows) {
|
||||||
|
// 当前页勾选 + 之前其它页保留下来的(不在 list 中的)
|
||||||
|
const curIds = new Set(this.picker.list.map(m => m.id))
|
||||||
|
const keepOther = this.picker.tempSelected.filter(m => !curIds.has(m.id))
|
||||||
|
this.picker.tempSelected = keepOther.concat(rows)
|
||||||
|
},
|
||||||
|
onPickerRowClick (row) {
|
||||||
|
const tbl = this.$refs.pickerTable
|
||||||
|
if (!tbl) return
|
||||||
|
const idx = this.picker.tempSelected.findIndex(m => m.id === row.id)
|
||||||
|
tbl.toggleRowSelection(row, idx === -1)
|
||||||
|
},
|
||||||
|
pickerRowClass ({ row }) {
|
||||||
|
return this.picker.tempSelected.find(m => m.id === row.id) ? 'picker-row-checked' : ''
|
||||||
|
},
|
||||||
|
confirmPicker () {
|
||||||
|
this.selectedMaterials = this.picker.tempSelected.slice()
|
||||||
|
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
|
||||||
|
this.materialPickerOpen = false
|
||||||
|
},
|
||||||
|
removeSelectedMaterial (id) {
|
||||||
|
this.selectedMaterials = this.selectedMaterials.filter(m => m.id !== id)
|
||||||
|
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
|
||||||
|
},
|
||||||
|
// ====== 新增物料 ======
|
||||||
|
openInlineNew () {
|
||||||
|
this.newMat.show = true
|
||||||
|
this.newMat.name = this.picker.kw || ''
|
||||||
|
this.newMat.model = ''
|
||||||
|
this.newMat.specifications = ''
|
||||||
|
this.newMat.brand = ''
|
||||||
|
this.newMat.unit = ''
|
||||||
|
this.newMat.inventory = 0
|
||||||
|
this.$nextTick(() => { this.$refs.newMatForm && this.$refs.newMatForm.clearValidate() })
|
||||||
|
},
|
||||||
|
submitNewMat () {
|
||||||
|
this.$refs.newMatForm.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
this.newMat.saving = true
|
||||||
|
const payload = {
|
||||||
|
name: this.newMat.name.trim(),
|
||||||
|
model: this.newMat.model || undefined,
|
||||||
|
specifications: this.newMat.specifications || undefined,
|
||||||
|
brand: this.newMat.brand || undefined,
|
||||||
|
unit: this.newMat.unit || undefined,
|
||||||
|
inventory: Number(this.newMat.inventory) || 0,
|
||||||
|
}
|
||||||
|
addOaWarehouse(payload).then(res => {
|
||||||
|
// 后端返回的 data 可能是 id 或对象,统一兜底再 get 一次
|
||||||
|
const newId = (res && (res.data && (res.data.id || typeof res.data === 'number'))) ? (res.data.id || res.data) : null
|
||||||
|
const finish = (full) => {
|
||||||
|
if (!full) return
|
||||||
|
// 直接放进当前页顶端 + 勾选
|
||||||
|
if (!this.picker.list.find(m => m.id === full.id)) {
|
||||||
|
this.picker.list.unshift(full)
|
||||||
|
}
|
||||||
|
if (!this.picker.tempSelected.find(m => m.id === full.id)) {
|
||||||
|
this.picker.tempSelected.push(full)
|
||||||
|
}
|
||||||
|
this.syncPickerSelectionUI()
|
||||||
|
this.newMat.show = false
|
||||||
|
this.$modal.msgSuccess('已新增并自动选中')
|
||||||
|
}
|
||||||
|
if (newId) {
|
||||||
|
getOaWarehouse(newId).then(r => finish(r.data))
|
||||||
|
} else {
|
||||||
|
// 兜底:用刚提交的字段拼一个临时对象
|
||||||
|
finish({ ...payload, id: Date.now() })
|
||||||
|
// 重新查一下保证 id 真实
|
||||||
|
this.loadPickerList()
|
||||||
|
}
|
||||||
|
}).finally(() => { this.newMat.saving = false })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
|
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
|
||||||
@@ -368,18 +583,6 @@ export default {
|
|||||||
if (file && file.url) window.open(file.url, '_blank')
|
if (file && file.url) window.open(file.url, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 展开行:加载该需求的入库批次
|
|
||||||
onExpandChange (row, expanded) {
|
|
||||||
if (!expanded || !expanded.length) return
|
|
||||||
const id = row.requirementId
|
|
||||||
if (this.batchMap[id]) return // 已有缓存
|
|
||||||
this.$set(this.batchLoading, id, true)
|
|
||||||
getRequirementBatches(id).then(res => {
|
|
||||||
this.$set(this.batchMap, id, res.data || [])
|
|
||||||
}).finally(() => {
|
|
||||||
this.$set(this.batchLoading, id, false)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async onStatusChange (row, newVal) {
|
async onStatusChange (row, newVal) {
|
||||||
row._updating = true
|
row._updating = true
|
||||||
// 如果后端需要字符串,可改为 String(newVal)
|
// 如果后端需要字符串,可改为 String(newVal)
|
||||||
@@ -453,6 +656,8 @@ export default {
|
|||||||
requesterId: undefined,
|
requesterId: undefined,
|
||||||
ownerId: undefined,
|
ownerId: undefined,
|
||||||
projectId: undefined,
|
projectId: undefined,
|
||||||
|
materialIds: undefined,
|
||||||
|
materialIdArr: [],
|
||||||
description: undefined,
|
description: undefined,
|
||||||
deadline: undefined,
|
deadline: undefined,
|
||||||
status: 0,
|
status: 0,
|
||||||
@@ -488,6 +693,7 @@ export default {
|
|||||||
handleAdd () {
|
handleAdd () {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.form.requesterId = this.$store.state.user.id;
|
this.form.requesterId = this.$store.state.user.id;
|
||||||
|
this.selectedMaterials = [];
|
||||||
this.open = true;
|
this.open = true;
|
||||||
this.title = "添加OA 需求";
|
this.title = "添加OA 需求";
|
||||||
},
|
},
|
||||||
@@ -504,10 +710,21 @@ export default {
|
|||||||
handleUpdate (row) {
|
handleUpdate (row) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.reset();
|
this.reset();
|
||||||
|
this.selectedMaterials = [];
|
||||||
const requirementId = row.requirementId || this.ids
|
const requirementId = row.requirementId || this.ids
|
||||||
getRequirements(requirementId).then(response => {
|
getRequirements(requirementId).then(response => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.form = response.data;
|
const data = response.data || {};
|
||||||
|
const ids = (data.materialIds || '').split(',').map(s => Number(s)).filter(n => !isNaN(n) && n)
|
||||||
|
data.materialIdArr = ids
|
||||||
|
this.form = data;
|
||||||
|
if (data.materials && data.materials.length) {
|
||||||
|
this.selectedMaterials = data.materials.slice()
|
||||||
|
} else if (ids.length) {
|
||||||
|
// 兜底:逐个拉
|
||||||
|
Promise.all(ids.map(id => getOaWarehouse(id).then(r => r.data).catch(() => null)))
|
||||||
|
.then(list => { this.selectedMaterials = list.filter(Boolean) })
|
||||||
|
}
|
||||||
this.open = true;
|
this.open = true;
|
||||||
this.title = "修改OA 需求";
|
this.title = "修改OA 需求";
|
||||||
});
|
});
|
||||||
@@ -517,6 +734,8 @@ export default {
|
|||||||
this.$refs["form"].validate(valid => {
|
this.$refs["form"].validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.buttonLoading = true;
|
this.buttonLoading = true;
|
||||||
|
const arr = Array.isArray(this.form.materialIdArr) ? this.form.materialIdArr : []
|
||||||
|
this.form.materialIds = arr.length ? arr.join(',') : null
|
||||||
if (this.form.requirementId != null) {
|
if (this.form.requirementId != null) {
|
||||||
updateRequirements(this.form).then(response => {
|
updateRequirements(this.form).then(response => {
|
||||||
this.$modal.msgSuccess("修改成功");
|
this.$modal.msgSuccess("修改成功");
|
||||||
@@ -652,6 +871,67 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover { color: #409eff; text-decoration: underline; }
|
&:hover { color: #409eff; text-decoration: underline; }
|
||||||
}
|
}
|
||||||
|
.mat-trigger {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
.mat-trigger-hint { font-size: 12px; color: #909399; }
|
||||||
|
}
|
||||||
|
.mat-remove {
|
||||||
|
margin-left: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #c0c4cc;
|
||||||
|
&:hover { color: #f56c6c; }
|
||||||
|
}
|
||||||
|
.picker-toolbar {
|
||||||
|
display: flex; align-items: center; margin-bottom: 10px;
|
||||||
|
.picker-tip { margin-left: auto; font-size: 12px; color: #606266; }
|
||||||
|
}
|
||||||
|
.picker-pager { text-align: right; margin-top: 10px; }
|
||||||
|
.new-mat-card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
::v-deep .el-card__header { padding: 6px 12px; background: #f5f7fa; }
|
||||||
|
.new-mat-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
font-size: 13px; color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::v-deep .picker-row-checked > td { background: #ecf5ff !important; }
|
||||||
|
.mat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
& + .mat-row { border-top: 1px dashed #ebeef5; }
|
||||||
|
.mat-name { flex: 1; color: #303133; }
|
||||||
|
.mat-model { color: #909399; margin-left: 2px; }
|
||||||
|
.mat-stock {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #f0f9eb; color: #67c23a;
|
||||||
|
&.low { background: #fef0f0; color: #f56c6c; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mat-stock-panel {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 3px;
|
||||||
|
.mat-stock-row {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
& + .mat-stock-row { border-top: 1px dashed #ebeef5; }
|
||||||
|
}
|
||||||
|
.mat-stock-name { flex: 1; min-width: 120px; color: #303133; }
|
||||||
|
.mat-stock-tag {
|
||||||
|
padding: 0 6px; border-radius: 3px;
|
||||||
|
background: #f0f9eb; color: #67c23a;
|
||||||
|
&.low { background: #fef0f0; color: #f56c6c; }
|
||||||
|
}
|
||||||
|
.mat-stock-meta { color: #909399; }
|
||||||
|
}
|
||||||
.accessory-link {
|
.accessory-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
|
|||||||
7
sql/requirement_add_material.sql
Normal file
7
sql/requirement_add_material.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- 采购需求关联物料库存(支持多物料,CSV)
|
||||||
|
ALTER TABLE oa_requirements
|
||||||
|
ADD COLUMN material_ids VARCHAR(1000) DEFAULT NULL COMMENT '关联物料ID CSV -> sys_oa_warehouse.id' AFTER project_id;
|
||||||
|
|
||||||
|
-- 已应用过单物料版本(material_id)的环境,执行下面这句把旧字段迁移并删除
|
||||||
|
-- UPDATE oa_requirements SET material_ids = CAST(material_id AS CHAR) WHERE material_id IS NOT NULL AND (material_ids IS NULL OR material_ids = '');
|
||||||
|
-- ALTER TABLE oa_requirements DROP COLUMN material_id;
|
||||||
Reference in New Issue
Block a user