Compare commits
5 Commits
cb1d385e77
...
578c15dc95
| Author | SHA1 | Date | |
|---|---|---|---|
| 578c15dc95 | |||
| 00dde3a896 | |||
| 2b0e335790 | |||
| b4971f057e | |||
| 66b3e4897f |
@ -353,3 +353,18 @@ def get_last_location():
|
||||
|
||||
location = BuyInboundService.get_last_location_by_base_id(base_id)
|
||||
return jsonify({"code": 200, "msg": "success", "data": {"location": location}})
|
||||
|
||||
|
||||
@inbound_buy_bp.route('/latest-record', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_latest_batch_record():
|
||||
"""
|
||||
按物料精准取最近一次采购入库记录(批号自增用)。
|
||||
返回 data: {'has_history': bool, 'mode': 'serial'|'batch', 'latest_batch': str}
|
||||
"""
|
||||
base_id = request.args.get('base_id', type=int)
|
||||
if not base_id:
|
||||
return jsonify({"code": 400, "msg": "base_id required"}), 400
|
||||
|
||||
data = BuyInboundService.get_latest_batch_record_by_base_id(base_id)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
|
||||
@ -267,14 +267,35 @@ class BomService:
|
||||
elif status == 'archived':
|
||||
query = query.filter(BomTable.is_archived == True)
|
||||
|
||||
# 关键词搜索
|
||||
# 关键词搜索:与 get_bom_list 保持同一口径——子查询匹配 (bom_no, version),
|
||||
# 同时覆盖 bom_no、父件名/规格、子件名/规格,保证任意关键字下 summary 计数与 list 明细绝对一致
|
||||
if keyword:
|
||||
kw = f'%{keyword.strip()}%'
|
||||
query = query.filter(or_(
|
||||
BomTable.bom_no.ilike(kw),
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw)
|
||||
))
|
||||
kw_child_alias = db.aliased(MaterialBase)
|
||||
match_subq = db.session.query(
|
||||
BomTable.bom_no,
|
||||
BomTable.version
|
||||
).join(
|
||||
MaterialBase, BomTable.parent_id == MaterialBase.id
|
||||
).outerjoin(
|
||||
kw_child_alias, BomTable.child_id == kw_child_alias.id
|
||||
).filter(
|
||||
or_(
|
||||
BomTable.bom_no.ilike(kw),
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw),
|
||||
kw_child_alias.name.ilike(kw),
|
||||
kw_child_alias.spec_model.ilike(kw)
|
||||
)
|
||||
).distinct().subquery()
|
||||
|
||||
query = query.join(
|
||||
match_subq,
|
||||
db.and_(
|
||||
BomTable.bom_no == match_subq.c.bom_no,
|
||||
BomTable.version == match_subq.c.version
|
||||
)
|
||||
)
|
||||
|
||||
query = query.group_by(MaterialBase.category) \
|
||||
.order_by(MaterialBase.category)
|
||||
@ -525,10 +546,16 @@ class BomService:
|
||||
"""
|
||||
仅更新某 BOM 版本(整组行)的启用状态,供列表/只读视图"启用/停用"开关使用。
|
||||
不触碰子件结构;成功后清除该 bom_no 的缓存。
|
||||
注意:只按 bom_no+version 精确匹配,不加 is_enabled/is_archived 等多余过滤——
|
||||
停用/归档中的版本只要记录存在即可继续改状态。
|
||||
"""
|
||||
bom_no = (bom_no or '').strip()
|
||||
version = (version or '').strip()
|
||||
if not bom_no or not version:
|
||||
raise ValueError('BOM编号/版本不能为空')
|
||||
rows = BomTable.query.filter_by(bom_no=bom_no, version=version).all()
|
||||
if not rows:
|
||||
raise ValueError('BOM 不存在')
|
||||
raise ValueError(f'BOM不存在(bom_no={bom_no}, version={version})')
|
||||
for rec in rows:
|
||||
rec.is_enabled = bool(is_enabled)
|
||||
db.session.commit()
|
||||
@ -542,9 +569,13 @@ class BomService:
|
||||
仅更新某 BOM 版本(整组行)的归档标记。
|
||||
归档=仍启用可查看/编辑,但不再作为其它 BOM 子件的引用候选;取消归档即恢复正常引用。
|
||||
"""
|
||||
bom_no = (bom_no or '').strip()
|
||||
version = (version or '').strip()
|
||||
if not bom_no or not version:
|
||||
raise ValueError('BOM编号/版本不能为空')
|
||||
rows = BomTable.query.filter_by(bom_no=bom_no, version=version).all()
|
||||
if not rows:
|
||||
raise ValueError('BOM 不存在')
|
||||
raise ValueError(f'BOM不存在(bom_no={bom_no}, version={version})')
|
||||
for rec in rows:
|
||||
rec.is_archived = bool(archived)
|
||||
db.session.commit()
|
||||
|
||||
@ -601,6 +601,28 @@ class BuyInboundService:
|
||||
return [r[0] for r in
|
||||
db.session.query(StockBuy.warehouse_location).filter(StockBuy.base_id == base_id).distinct().all()]
|
||||
|
||||
@staticmethod
|
||||
def get_latest_batch_record_by_base_id(base_id):
|
||||
"""
|
||||
按物料精准取“最近一次采购入库记录”(SQL 直接 filter base_id + limit,O(1)、不分页)。
|
||||
用于采购入库表单的批号自增:避免以前端拉 getBuyList 前 1000 条再过滤造成的
|
||||
“历史被截断 → 误判无历史 → 批号退回 000001/重复入库被拦”问题。
|
||||
返回: {'has_history': bool, 'mode': 'serial'|'batch', 'latest_batch': str}
|
||||
"""
|
||||
if not base_id:
|
||||
return {'has_history': False, 'mode': 'batch', 'latest_batch': ''}
|
||||
latest = StockBuy.query.filter(
|
||||
StockBuy.base_id == base_id
|
||||
).order_by(
|
||||
StockBuy.in_date.desc().nullslast(),
|
||||
StockBuy.id.desc()
|
||||
).first()
|
||||
if latest is None:
|
||||
return {'has_history': False, 'mode': 'batch', 'latest_batch': ''}
|
||||
if latest.serial_number:
|
||||
return {'has_history': True, 'mode': 'serial', 'latest_batch': latest.batch_number or ''}
|
||||
return {'has_history': True, 'mode': 'batch', 'latest_batch': latest.batch_number or ''}
|
||||
|
||||
@staticmethod
|
||||
def get_last_location_by_base_id(base_id):
|
||||
"""
|
||||
|
||||
@ -54,6 +54,15 @@ export function searchMaterialBase(keyword: string, page: number = 1, company?:
|
||||
})
|
||||
}
|
||||
|
||||
// 6.1 按物料精准取“最近一次采购入库记录”(批号自增用;不再全局拉前 N 条再前端过滤)
|
||||
export function getBuyLatestRecord(baseId: number) {
|
||||
return request({
|
||||
url: '/inbound/buy/latest-record',
|
||||
method: 'get',
|
||||
params: { base_id: baseId }
|
||||
})
|
||||
}
|
||||
|
||||
// 7. 文件上传
|
||||
export function uploadFile(data: FormData) {
|
||||
return request({
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
:title="group.category + ' (' + group.count + ')'"
|
||||
:name="group.category"
|
||||
>
|
||||
<el-table v-if="activeCategories.includes(group.category)" :data="group.items" border style="width: 100%">
|
||||
<el-table v-if="isCategoryActive(group.category)" :data="group.items" border style="width: 100%">
|
||||
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span style="cursor: pointer; color: #409EFF;" @click="handleView(row)">{{ row.bom_no }}</span>
|
||||
@ -424,6 +424,9 @@ const isSaveAsMode = ref(false)
|
||||
const isReadOnlyMode = ref(false)
|
||||
let originalVersion = ''
|
||||
let currentBomNo = ''
|
||||
// ★ 版本升级的响应式基准(plain let 不是响应式,computed 读不到变化 → 1.1 升次版本会算出错的候选)
|
||||
const upgradeBaseVersion = ref('')
|
||||
const upgradeBaseBomNo = ref('')
|
||||
let originalChildren: ChildRow[] = []
|
||||
let pendingDraftBomNo = ''
|
||||
let pendingDraftVersion = ''
|
||||
@ -441,7 +444,15 @@ interface GroupSummary { category: string; count: number }
|
||||
const groupSummary = ref<GroupSummary[]>([])
|
||||
const groupCache = ref<Map<string, any[]>>(new Map())
|
||||
const groupLoadingMap = ref<Map<string, boolean>>(new Map())
|
||||
const activeCategories = ref<string[]>([])
|
||||
// ★ el-collapse 手风琴(accordion=true)时 v-model 是单个 String,多开时才是 Array —— 统一按数组读写
|
||||
const activeCategories = ref<string | string[]>([])
|
||||
const activeList = (): string[] =>
|
||||
Array.isArray(activeCategories.value) ? activeCategories.value : (activeCategories.value ? [activeCategories.value] : [])
|
||||
const isCategoryActive = (c: string) => activeList().includes(c)
|
||||
const setActiveList = (arr: string[]) => {
|
||||
if (isAccordion.value) activeCategories.value = arr.length ? arr[0] : ''
|
||||
else activeCategories.value = arr
|
||||
}
|
||||
const lastKeyword = ref('')
|
||||
|
||||
const groupedData = computed(() => {
|
||||
@ -609,11 +620,11 @@ const form = reactive({
|
||||
const pureBomNo = computed(() => form.bom_no)
|
||||
|
||||
const versionOptions = computed(() => {
|
||||
const ver = originalVersion || 'V1.0'
|
||||
const ver = upgradeBaseVersion.value || 'V1.0'
|
||||
const allItems: any[] = []
|
||||
groupCache.value.forEach((items) => allItems.push(...items))
|
||||
const occupiedVersions = new Set(
|
||||
allItems.filter((item: any) => item.bom_no === currentBomNo).map((item: any) => item.version)
|
||||
allItems.filter((item: any) => item.bom_no === upgradeBaseBomNo.value).map((item: any) => item.version)
|
||||
)
|
||||
const getNextMinor = (baseMajor: number, baseMinor: number): string => {
|
||||
let minor = baseMinor + 1
|
||||
@ -646,9 +657,9 @@ const rules = reactive<FormRules>({
|
||||
const dialogTitle = ref('新建 BOM')
|
||||
|
||||
const handleSearch = () => {
|
||||
activeCategories.value = []
|
||||
groupCache.value = new Map()
|
||||
isAccordion.value = true // 搜索/筛选重置回默认手风琴模式
|
||||
groupCache.value = new Map()
|
||||
setActiveList([]) // 手风琴下折叠为 '',多开下折叠为 []
|
||||
fetchBomSummary()
|
||||
}
|
||||
|
||||
@ -664,7 +675,7 @@ const fetchBomSummary = async () => {
|
||||
if (res?.code === 200) {
|
||||
groupSummary.value = res.data ?? []
|
||||
if (searchKeyword.value !== lastKeyword.value) {
|
||||
activeCategories.value = []
|
||||
setActiveList([])
|
||||
lastKeyword.value = searchKeyword.value
|
||||
}
|
||||
}
|
||||
@ -680,15 +691,14 @@ const expandAllGroups = () => {
|
||||
}
|
||||
// ★ 全部折叠:清空面板,并恢复手风琴模式
|
||||
const collapseAllGroups = () => {
|
||||
activeCategories.value = []
|
||||
isAccordion.value = true
|
||||
setActiveList([])
|
||||
}
|
||||
|
||||
// ★ 展开分组时懒加载该分类下的 BOM
|
||||
const loadGroupItems = async (category: string) => {
|
||||
if (groupCache.value.has(category)) return
|
||||
if (groupLoadingMap.value.get(category)) return
|
||||
|
||||
// ★ 静默获取单个分类的最新数据并“覆盖”缓存:
|
||||
// 不清空该分类缓存、请求失败则保留旧数据(保证面板绝不闪白/变空)。
|
||||
const fetchCategoryChildren = async (category: string): Promise<boolean> => {
|
||||
if (groupLoadingMap.value.get(category)) return false
|
||||
groupLoadingMap.value.set(category, true)
|
||||
try {
|
||||
const params: any = {
|
||||
@ -703,8 +713,19 @@ const loadGroupItems = async (category: string) => {
|
||||
if (res?.code === 200) {
|
||||
groupCache.value.set(category, res.data?.items ?? [])
|
||||
}
|
||||
} catch (e) { console.error(`加载BOM分类 [${category}] 失败`, e) }
|
||||
finally { groupLoadingMap.value.set(category, false) }
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error(`加载BOM分类 [${category}] 失败`, e)
|
||||
return false
|
||||
} finally {
|
||||
groupLoadingMap.value.set(category, false)
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 展开分组时懒加载该分类下的 BOM(仅在无缓存时拉取)
|
||||
const loadGroupItems = async (category: string) => {
|
||||
if (groupCache.value.has(category)) return
|
||||
await fetchCategoryChildren(category)
|
||||
}
|
||||
|
||||
// ★ collapse @change 事件 → 触发懒加载
|
||||
@ -713,6 +734,47 @@ const handleCollapseChange = (val: string | string[]) => {
|
||||
cats.forEach(c => loadGroupItems(c))
|
||||
}
|
||||
|
||||
// ★ 启停/归档/删除等行操作后的平滑刷新:
|
||||
// - 绝不清空整体缓存,也绝不清空/关闭任何已展开面板;
|
||||
// - 先更新分组摘要计数,再对“当前已展开”的分类逐个静默重取最新数据覆盖缓存;
|
||||
// - 不符合当前筛选的行会因重取自然消失、符合的保留;单次请求失败则沿用旧数据,面板不闪白。
|
||||
const reloadCurrentGroups = async () => {
|
||||
// 1. 获取最新分组摘要
|
||||
await fetchBomSummary();
|
||||
|
||||
// 2. 有效分类集合(防御:摘要可能为空)
|
||||
const validCategoryNames = (groupSummary.value || []).map(g => g && g.category);
|
||||
|
||||
// 3. 兼容手风琴(String)与多开(Array),统一转数组;再过滤掉已归零分类
|
||||
const currentActiveList = Array.isArray(activeCategories.value)
|
||||
? activeCategories.value
|
||||
: (activeCategories.value ? [activeCategories.value] : []);
|
||||
const validActiveList = currentActiveList.filter(c => validCategoryNames.includes(c));
|
||||
|
||||
// 4. 按当前模式安全写回(手风琴只保留第一个 → 触发折叠动画;多开写回数组)
|
||||
if (isAccordion.value) {
|
||||
activeCategories.value = validActiveList.length > 0 ? validActiveList[0] : '';
|
||||
} else {
|
||||
activeCategories.value = validActiveList;
|
||||
}
|
||||
|
||||
// 5. 确定要刷新的打开面板
|
||||
const opened = [...validActiveList];
|
||||
|
||||
// 6. 暴力清理不需要的旧缓存(遍历 cache 的 keys,而不是 summary)
|
||||
for (const key of Array.from((groupCache.value && groupCache.value.keys ? groupCache.value.keys() : []) || [])) {
|
||||
if (!opened.includes(key)) {
|
||||
groupCache.value.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 重新拉取仍在打开状态的分类明细
|
||||
await Promise.all((opened || []).map(c => fetchCategoryChildren(c)));
|
||||
|
||||
// 8. [关键] 强制触发 Vue 对 Map 的深度响应式更新
|
||||
groupCache.value = new Map(groupCache.value || new Map());
|
||||
}
|
||||
|
||||
const onParentChange = (val: number) => {}
|
||||
|
||||
const onChildChange = (val: number | null, index: number) => {
|
||||
@ -949,10 +1011,15 @@ const handleSaveAs = async (row: any) => {
|
||||
form.bom_no = (raw.parent_spec || row.bom_no).split('/')[0].trim()
|
||||
form.remark = raw.remark || ''
|
||||
form.versionUpgradeType = 'minor'
|
||||
form.version = versionOptions.value?.minor || 'V1.1'
|
||||
|
||||
// ★ 先锁定升级基准(原版本号 + 该 BOM),再计算目标版本,保证 V1.1 → V1.2 等正确且跳过已占用号
|
||||
originalVersion = raw.version || ''
|
||||
currentBomNo = row.bom_no
|
||||
upgradeBaseVersion.value = originalVersion
|
||||
upgradeBaseBomNo.value = currentBomNo
|
||||
const upgradeOpts = versionOptions.value
|
||||
form.version = upgradeOpts?.minor || upgradeOpts?.major || 'V1.1'
|
||||
|
||||
originalChildren = JSON.parse(JSON.stringify(form.children))
|
||||
originalDraftHash.value = getDraftHash()
|
||||
|
||||
@ -989,14 +1056,11 @@ const loadDetail = async (bomNo: string, version: string) => {
|
||||
parentNameInput.value = data.parent_name || '未知产品'
|
||||
}
|
||||
|
||||
if (data.parent_spec) {
|
||||
form.bom_no = (data.parent_spec || '').split('/')[0].trim()
|
||||
} else {
|
||||
form.bom_no = bomNo.split('/')[0].trim()
|
||||
}
|
||||
// ★ 只读视图的状态开关必须用后端返回的权威 bom_no/version,
|
||||
// 不要再用 parent_spec 首位 token 去反推(规格首位可能不等于 BOM 编号 → /status 查不到 → 400)
|
||||
form.bom_no = data.bom_no || bomNo
|
||||
form.remark = data.remark || ''
|
||||
// 查看/只读视图的独立启停需要版本与当前状态
|
||||
form.version = version || data.version || 'V1.0'
|
||||
form.version = data.version || version || 'V1.0'
|
||||
form.is_enabled = data.is_enabled !== false
|
||||
}
|
||||
} catch (e) {
|
||||
@ -1006,34 +1070,70 @@ const loadDetail = async (bomNo: string, version: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 前端本地“秒移除”:后端确认成功后,按“新状态 + 当前筛选”判断该行是否还可见,
|
||||
// 不可见则立即从各分类缓存数组中踢掉并强制 Map 换引用(不等明细接口,列表立刻消失)
|
||||
const applyLocalRowState = (bomNo: string, version: string, patch: { is_enabled?: boolean; is_archived?: boolean }) => {
|
||||
const status = bomStatusFilter.value
|
||||
let changed = false
|
||||
for (const [, list] of Array.from((groupCache.value && groupCache.value.entries ? groupCache.value.entries() : []) || [])) {
|
||||
if (!Array.isArray(list)) continue // 防御:缓存某项可能为空
|
||||
const idx = list.findIndex((i: any) => i && i.bom_no === bomNo && i.version === version)
|
||||
if (idx === -1) continue
|
||||
const item = list[idx]
|
||||
if (!item) continue
|
||||
if (patch.is_enabled !== undefined) item.is_enabled = patch.is_enabled
|
||||
if (patch.is_archived !== undefined) item.is_archived = patch.is_archived
|
||||
let visible = true
|
||||
if (status === 'enabled') visible = !!item.is_enabled && !item.is_archived
|
||||
else if (status === 'disabled') visible = item.is_enabled === false
|
||||
else if (status === 'archived') visible = !!item.is_archived
|
||||
if (!visible) { list.splice(idx, 1); changed = true }
|
||||
}
|
||||
if (changed) groupCache.value = new Map(groupCache.value)
|
||||
}
|
||||
|
||||
// ★ 启用/停用开关:查看/只读态下独立更新状态(不触发整表单保存);
|
||||
// 新建/编辑/另存模式则仅记录到 form,随 submitForm 一并保存。
|
||||
const onEnabledStatusChange = async () => {
|
||||
if (!isReadOnlyMode.value) return
|
||||
|
||||
if (!pureBomNo.value || !form.version) {
|
||||
const curBomNo = String(form.bom_no || '').trim()
|
||||
const curVersion = String(form.version || '').trim()
|
||||
if (!curBomNo || !curVersion) {
|
||||
ElMessage.warning('缺少 BOM 编号/版本,无法更新状态')
|
||||
return
|
||||
}
|
||||
const target = !!form.is_enabled
|
||||
|
||||
// ① 接口层:只负责提交;只有这一层失败才提示“状态更新失败”
|
||||
saving.value = true
|
||||
try {
|
||||
const res: any = await updateBomStatus({ bom_no: pureBomNo.value, version: form.version, is_enabled: target })
|
||||
const res: any = await updateBomStatus({ bom_no: curBomNo, version: curVersion, is_enabled: target })
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success(target ? '已启用' : '已停用')
|
||||
// 刷新左侧列表摘要(计数与状态列)
|
||||
groupCache.value = new Map()
|
||||
fetchBomSummary()
|
||||
form.is_enabled = target
|
||||
} else {
|
||||
form.is_enabled = !target
|
||||
ElMessage.error(res?.msg || '状态更新失败')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('启用/停用接口异常:', e)
|
||||
form.is_enabled = !target
|
||||
ElMessage.error('状态更新失败')
|
||||
ElMessage.error((e as any)?.response ? '状态更新失败' : '网络异常,状态可能未更新,请重试')
|
||||
return
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
// ② 界面层:状态已成功,本地秒移除 + 整体同步;此处异常绝不回退成“状态更新失败”
|
||||
try {
|
||||
applyLocalRowState(curBomNo, curVersion, { is_enabled: target })
|
||||
await reloadCurrentGroups()
|
||||
} catch (e) {
|
||||
console.error('状态已更新,但界面刷新异常:', e)
|
||||
ElMessage.warning('状态已更新,界面刷新异常,请手动刷新查看')
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 归档/取消归档:仍启用可编辑,但不再作为其它 BOM 子件的版本候选
|
||||
@ -1045,25 +1145,60 @@ const handleArchive = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(tip, '提示', { type: 'warning' })
|
||||
} catch (e) { return }
|
||||
// ① 接口层
|
||||
try {
|
||||
const res: any = await updateBomArchive({ bom_no: row.bom_no, version: row.version, is_archived: target })
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success(target ? '已归档' : '已取消归档')
|
||||
groupCache.value = new Map()
|
||||
fetchBomSummary()
|
||||
} else {
|
||||
ElMessage.error(res?.msg || '操作失败')
|
||||
return
|
||||
}
|
||||
} catch (e) { ElMessage.error('归档操作失败') }
|
||||
} catch (e) {
|
||||
console.error('归档接口异常:', e)
|
||||
ElMessage.error((e as any)?.response ? '操作失败' : '网络异常,请重试')
|
||||
return
|
||||
}
|
||||
|
||||
// ② 界面层
|
||||
try {
|
||||
applyLocalRowState(row.bom_no, row.version, { is_archived: target })
|
||||
await reloadCurrentGroups()
|
||||
} catch (e) {
|
||||
console.error('归档成功,但界面刷新异常:', e)
|
||||
ElMessage.warning('操作已生效,界面刷新异常,请手动刷新查看')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row: BomItem) => {
|
||||
ElMessageBox.confirm(`确定删除 ${row.bom_no} (${row.version}) 吗?`, '警告', { type: 'warning' })
|
||||
.then(async () => {
|
||||
let deleted = false
|
||||
try {
|
||||
const res = await deleteBom(row.bom_no, row.version)
|
||||
if (res.code === 200) { ElMessage.success('删除成功'); groupCache.value = new Map(); fetchBomSummary() }
|
||||
} catch (e) {}
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
// 删除不受筛选限制:本地移除该行(带数组守卫)
|
||||
for (const [, list] of Array.from((groupCache.value && groupCache.value.entries ? groupCache.value.entries() : []) || [])) {
|
||||
if (!Array.isArray(list)) continue
|
||||
const i = list.findIndex((it: any) => it && it.bom_no === row.bom_no && it.version === row.version)
|
||||
if (i > -1) list.splice(i, 1)
|
||||
}
|
||||
groupCache.value = new Map(groupCache.value)
|
||||
deleted = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('删除接口异常:', e)
|
||||
ElMessage.error((e as any)?.response ? '删除失败' : '网络异常,请重试')
|
||||
}
|
||||
if (deleted) {
|
||||
try {
|
||||
await reloadCurrentGroups()
|
||||
} catch (e) {
|
||||
console.error('删除成功,但界面刷新异常:', e)
|
||||
ElMessage.warning('已删除,界面刷新异常,请手动刷新查看')
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
@ -1080,6 +1215,8 @@ const resetForm = () => {
|
||||
isReadOnlyMode.value = false
|
||||
originalVersion = ''
|
||||
currentBomNo = ''
|
||||
upgradeBaseVersion.value = ''
|
||||
upgradeBaseBomNo.value = ''
|
||||
childSearchKeyword.value = ''
|
||||
parentNameInput.value = ''
|
||||
originalDraftHash.value = ''
|
||||
@ -1157,8 +1294,8 @@ const submitForm = async () => {
|
||||
localStorage.removeItem('pending_bom_draft_version')
|
||||
originalDraftHash.value = ''
|
||||
dialogVisible.value = false
|
||||
groupCache.value = new Map()
|
||||
fetchBomSummary()
|
||||
// 关窗后仅重载已展开分类,避免列表空白
|
||||
reloadCurrentGroups()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '保存失败')
|
||||
}
|
||||
|
||||
@ -799,7 +799,8 @@ import {
|
||||
getSupplierSuggestions,
|
||||
getUserSuggestions,
|
||||
getLocationSuggestions,
|
||||
getFilterOptions
|
||||
getFilterOptions,
|
||||
getBuyLatestRecord
|
||||
} from '@/api/inbound/buy'
|
||||
import { getApprovedUnstockedRequests } from '@/api/purchase'
|
||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||
@ -1352,24 +1353,25 @@ const rules = computed(() => {
|
||||
|
||||
const checkHistoryAndSetMode = async (baseId: number) => {
|
||||
try {
|
||||
const res: any = await getBuyList({page: 1, pageSize: 1000})
|
||||
const historyItems = (res.data.items || []).filter((item: any) => item.base_id === baseId)
|
||||
// ★ 改用后端“按物料精准取最近一条”接口(O(1)、不分页、不受历史行数/前1000条截断影响)
|
||||
const res: any = await getBuyLatestRecord(baseId)
|
||||
const rec = res?.data || {}
|
||||
|
||||
if (historyItems.length > 0) {
|
||||
if (rec.has_history) {
|
||||
modeLocked.value = true
|
||||
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
||||
if (latest.serial_number) {
|
||||
if (rec.mode === 'serial') {
|
||||
entryMode.value = 'serial'
|
||||
form.serial_number = ''
|
||||
form.batch_number = ''
|
||||
} else {
|
||||
entryMode.value = 'batch'
|
||||
form.serial_number = ''
|
||||
form.batch_number = incrementBatchNumber(latest.batch_number || '000000')
|
||||
form.batch_number = incrementBatchNumber(rec.latest_batch || '000000')
|
||||
}
|
||||
} else {
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.serial_number = ''
|
||||
form.batch_number = '000001'
|
||||
}
|
||||
if (formRef.value) {
|
||||
@ -1379,6 +1381,7 @@ const checkHistoryAndSetMode = async (baseId: number) => {
|
||||
} catch (e) {
|
||||
modeLocked.value = false
|
||||
entryMode.value = 'batch'
|
||||
form.serial_number = ''
|
||||
form.batch_number = '000001'
|
||||
}
|
||||
}
|
||||
@ -1916,10 +1919,11 @@ const confirmPurchaseImport = () => {
|
||||
isInspectionRequired: mat.is_inspection_required
|
||||
}]
|
||||
} else if (po.base_id) {
|
||||
// 如果 material 为空但 base_id 有值,尝试通过 base_id 查询
|
||||
// 这里简单处理:直接设置 base_id 让用户手动搜索
|
||||
// 如果 material 为空但 base_id 有值:仍按 base_id 续批号/带历史库位,物料详情让用户手动搜索确认
|
||||
form.base_id = po.base_id
|
||||
ElMessage.info('该采购单未附带物料详情,已填充 base_id,请手动搜索物料确认')
|
||||
checkHistoryAndSetMode(po.base_id)
|
||||
fetchLastLocation(po.base_id)
|
||||
ElMessage.info('该采购单未附带物料详情,已按物料填充批号/库位,请手动搜索确认物料信息')
|
||||
} else {
|
||||
ElMessage.warning('该采购单未关联基础物料,无法自动填充物料信息')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user