feat(bom): 归档业务闭环——状态筛选+归档/取消归档入口
- get_bom_list/get_bom_summary 状态过滤支持 archived,enabled 语义改为“启用且未归档” - get_bom_list 输出补 is_archived;新增 update_archived 与 POST /bom/archive(整组切换归档、清缓存、审计) - “未指定版本自动取最新”硬化为仅认启用且未归档(get_bom_detail / get_bom_no_by_parent) - 前端:状态按钮组加“归档”;列表状态列区分归档;操作列加“归档/取消归档”按钮
This commit is contained in:
@ -136,6 +136,35 @@ def update_bom_status():
|
|||||||
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
|
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== BOM 归档/取消归档(仍启用可编辑,但不作为其它 BOM 子件引用候选) ====================
|
||||||
|
@bom_bp.route('/archive', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
@permission_required('bom_manage:operation')
|
||||||
|
@audit_log(
|
||||||
|
module='BOM管理',
|
||||||
|
action='归档/取消归档',
|
||||||
|
get_target_name_fn=lambda: (request.get_json() or {}).get('bom_no')
|
||||||
|
)
|
||||||
|
def update_bom_archive():
|
||||||
|
"""切换某 BOM 版本(整组)的归档状态。"""
|
||||||
|
try:
|
||||||
|
data = request.get_json() or {}
|
||||||
|
bom_no = data.get('bom_no')
|
||||||
|
version = data.get('version')
|
||||||
|
is_archived = data.get('is_archived')
|
||||||
|
if not bom_no or not version:
|
||||||
|
return jsonify({'code': 400, 'msg': 'bom_no 与 version 不能为空'}), 400
|
||||||
|
if not isinstance(is_archived, bool):
|
||||||
|
return jsonify({'code': 400, 'msg': 'is_archived 必须为布尔值'}), 400
|
||||||
|
BomService.update_archived(bom_no, version, is_archived)
|
||||||
|
return jsonify({'code': 200, 'msg': '更新成功'})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f'更新BOM归档状态失败: {str(e)}')
|
||||||
|
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
|
||||||
|
|
||||||
|
|
||||||
@bom_bp.route('/detail/<path:bom_no>', methods=['GET'])
|
@bom_bp.route('/detail/<path:bom_no>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@permission_required('bom_manage')
|
@permission_required('bom_manage')
|
||||||
|
|||||||
@ -133,6 +133,7 @@ class BomService:
|
|||||||
MaterialBase.spec_model.label('parent_spec'),
|
MaterialBase.spec_model.label('parent_spec'),
|
||||||
MaterialBase.category.label('parent_category'),
|
MaterialBase.category.label('parent_category'),
|
||||||
BomTable.is_enabled,
|
BomTable.is_enabled,
|
||||||
|
BomTable.is_archived,
|
||||||
func.count(BomTable.child_id).label('child_count'),
|
func.count(BomTable.child_id).label('child_count'),
|
||||||
func.string_agg(child_alias.name, ', ').label('child_names'),
|
func.string_agg(child_alias.name, ', ').label('child_names'),
|
||||||
func.string_agg(child_alias.spec_model, ', ').label('child_specs')
|
func.string_agg(child_alias.spec_model, ', ').label('child_specs')
|
||||||
@ -143,14 +144,17 @@ class BomService:
|
|||||||
).group_by(
|
).group_by(
|
||||||
BomTable.bom_no, BomTable.version, BomTable.parent_id,
|
BomTable.bom_no, BomTable.version, BomTable.parent_id,
|
||||||
MaterialBase.name, MaterialBase.spec_model, MaterialBase.category,
|
MaterialBase.name, MaterialBase.spec_model, MaterialBase.category,
|
||||||
BomTable.is_enabled
|
BomTable.is_enabled, BomTable.is_archived
|
||||||
)
|
)
|
||||||
|
|
||||||
# 状态过滤:status 优先于旧参数 active_only(enabled/disabled 均为精确过滤)
|
# 状态过滤:status 优先于旧参数 active_only
|
||||||
|
# enabled=启用且未归档(在用);disabled=停用;archived=归档(保留可见/编辑,但不作子件引用候选)
|
||||||
if status == 'enabled':
|
if status == 'enabled':
|
||||||
query = query.filter(BomTable.is_enabled == True)
|
query = query.filter(BomTable.is_enabled == True, BomTable.is_archived == False)
|
||||||
elif status == 'disabled':
|
elif status == 'disabled':
|
||||||
query = query.filter(BomTable.is_enabled == False)
|
query = query.filter(BomTable.is_enabled == False)
|
||||||
|
elif status == 'archived':
|
||||||
|
query = query.filter(BomTable.is_archived == True)
|
||||||
elif active_only:
|
elif active_only:
|
||||||
query = query.filter(BomTable.is_enabled == True)
|
query = query.filter(BomTable.is_enabled == True)
|
||||||
|
|
||||||
@ -211,6 +215,7 @@ class BomService:
|
|||||||
'parent_spec': row.parent_spec or '',
|
'parent_spec': row.parent_spec or '',
|
||||||
'parent_category': row.parent_category or '',
|
'parent_category': row.parent_category or '',
|
||||||
'is_enabled': row.is_enabled,
|
'is_enabled': row.is_enabled,
|
||||||
|
'is_archived': row.is_archived,
|
||||||
'child_count': row.child_count,
|
'child_count': row.child_count,
|
||||||
'child_names': row.child_names or '',
|
'child_names': row.child_names or '',
|
||||||
'child_specs': row.child_specs or ''
|
'child_specs': row.child_specs or ''
|
||||||
@ -254,10 +259,13 @@ class BomService:
|
|||||||
query = query.filter(MaterialBase.company_name == company_limit)
|
query = query.filter(MaterialBase.company_name == company_limit)
|
||||||
|
|
||||||
# 状态过滤(按 (bom_no, version) 维度计数,故过滤粒度到行即可,行状态同版本一致)
|
# 状态过滤(按 (bom_no, version) 维度计数,故过滤粒度到行即可,行状态同版本一致)
|
||||||
|
# enabled=启用且未归档;disabled=停用;archived=归档
|
||||||
if status == 'enabled':
|
if status == 'enabled':
|
||||||
query = query.filter(BomTable.is_enabled == True)
|
query = query.filter(BomTable.is_enabled == True, BomTable.is_archived == False)
|
||||||
elif status == 'disabled':
|
elif status == 'disabled':
|
||||||
query = query.filter(BomTable.is_enabled == False)
|
query = query.filter(BomTable.is_enabled == False)
|
||||||
|
elif status == 'archived':
|
||||||
|
query = query.filter(BomTable.is_archived == True)
|
||||||
|
|
||||||
# 关键词搜索
|
# 关键词搜索
|
||||||
if keyword:
|
if keyword:
|
||||||
@ -313,8 +321,12 @@ class BomService:
|
|||||||
if version:
|
if version:
|
||||||
query = query.filter(BomTable.version == version)
|
query = query.filter(BomTable.version == version)
|
||||||
else:
|
else:
|
||||||
latest_ver = db.session.query(BomTable.version).filter_by(bom_no=bom_no) \
|
# ★ 自动"取最新"只认"启用且未归档",避免默认命中已停用/已归档版本
|
||||||
.order_by(BomTable.version.desc()).limit(1).scalar()
|
latest_ver = db.session.query(BomTable.version).filter(
|
||||||
|
BomTable.bom_no == bom_no,
|
||||||
|
BomTable.is_enabled == True,
|
||||||
|
BomTable.is_archived == False
|
||||||
|
).order_by(BomTable.version.desc()).limit(1).scalar()
|
||||||
if not latest_ver:
|
if not latest_ver:
|
||||||
return None
|
return None
|
||||||
query = query.filter(BomTable.version == latest_ver)
|
query = query.filter(BomTable.version == latest_ver)
|
||||||
@ -524,6 +536,22 @@ class BomService:
|
|||||||
logger.info(f"[BOM Cache] update_enabled → 缓存已失效 bom_no={bom_no} version={version} enabled={is_enabled}")
|
logger.info(f"[BOM Cache] update_enabled → 缓存已失效 bom_no={bom_no} version={version} enabled={is_enabled}")
|
||||||
return len(rows)
|
return len(rows)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_archived(bom_no, version, archived):
|
||||||
|
"""
|
||||||
|
仅更新某 BOM 版本(整组行)的归档标记。
|
||||||
|
归档=仍启用可查看/编辑,但不再作为其它 BOM 子件的引用候选;取消归档即恢复正常引用。
|
||||||
|
"""
|
||||||
|
rows = BomTable.query.filter_by(bom_no=bom_no, version=version).all()
|
||||||
|
if not rows:
|
||||||
|
raise ValueError('BOM 不存在')
|
||||||
|
for rec in rows:
|
||||||
|
rec.is_archived = bool(archived)
|
||||||
|
db.session.commit()
|
||||||
|
_cache_delete(bom_no, version)
|
||||||
|
logger.info(f"[BOM Cache] update_archived → 缓存已失效 bom_no={bom_no} version={version} archived={archived}")
|
||||||
|
return len(rows)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_bom_with_stock_by_bom_no(bom_no, version=None):
|
def get_bom_with_stock_by_bom_no(bom_no, version=None):
|
||||||
"""
|
"""
|
||||||
@ -580,7 +608,12 @@ class BomService:
|
|||||||
# ====================== 兼容旧接口 ======================
|
# ====================== 兼容旧接口 ======================
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_bom_no_by_parent(parent_id):
|
def get_bom_no_by_parent(parent_id):
|
||||||
row = BomTable.query.filter_by(parent_id=parent_id).order_by(BomTable.version.desc()).first()
|
# ★ 仅取"启用且未归档"的最新配方,避免默认命中已停用/已归档版本
|
||||||
|
row = BomTable.query.filter(
|
||||||
|
BomTable.parent_id == parent_id,
|
||||||
|
BomTable.is_enabled == True,
|
||||||
|
BomTable.is_archived == False
|
||||||
|
).order_by(BomTable.version.desc()).first()
|
||||||
return row.bom_no if row else None
|
return row.bom_no if row else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -61,6 +61,15 @@ export function updateBomStatus(data: { bom_no: string; version: string; is_enab
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 归档/取消归档(仍启用可编辑,但不再作为其它 BOM 子件的引用候选)
|
||||||
|
export function updateBomArchive(data: { bom_no: string; version: string; is_archived: boolean }) {
|
||||||
|
return request({
|
||||||
|
url: '/v1/bom/archive',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 保存BOM
|
// 保存BOM
|
||||||
export function saveBom(data: any) {
|
export function saveBom(data: any) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
<el-radio-button :label="''">全部</el-radio-button>
|
<el-radio-button :label="''">全部</el-radio-button>
|
||||||
<el-radio-button label="enabled">启用</el-radio-button>
|
<el-radio-button label="enabled">启用</el-radio-button>
|
||||||
<el-radio-button label="disabled">停用</el-radio-button>
|
<el-radio-button label="disabled">停用</el-radio-button>
|
||||||
|
<el-radio-button label="archived">归档</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<el-button @click="expandAllGroups" size="small" style="margin-right: 6px;">全部展开</el-button>
|
<el-button @click="expandAllGroups" size="small" style="margin-right: 6px;">全部展开</el-button>
|
||||||
<el-button @click="collapseAllGroups" size="small" style="margin-right: 10px;">全部折叠</el-button>
|
<el-button @click="collapseAllGroups" size="small" style="margin-right: 10px;">全部折叠</el-button>
|
||||||
@ -55,13 +56,15 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
|
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.is_enabled ? 'success' : 'danger'">{{ row.is_enabled ? '启用' : '禁用' }}</el-tag>
|
<el-tag v-if="row.is_archived" type="info">归档</el-tag>
|
||||||
|
<el-tag v-else :type="row.is_enabled ? 'success' : 'danger'">{{ row.is_enabled ? '启用' : '停用' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="hasColumnPermission('child_count')" prop="child_count" label="子件数" width="80" align="center" />
|
<el-table-column v-if="hasColumnPermission('child_count')" prop="child_count" label="子件数" width="80" align="center" />
|
||||||
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="160" align="center" fixed="right">
|
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="230" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="success" link @click="handleSaveAs(row)">另存为</el-button>
|
<el-button type="success" link @click="handleSaveAs(row)">另存为</el-button>
|
||||||
|
<el-button type="warning" link @click="handleArchive(row)">{{ row.is_archived ? '取消归档' : '归档' }}</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -301,7 +304,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||||
import { Plus, Search, EditPen, Upload } from '@element-plus/icons-vue'
|
import { Plus, Search, EditPen, Upload } from '@element-plus/icons-vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { getBomList, getBomSummary, getBomDetail, saveBom, deleteBom, getDraftDetail, saveDraft, publishDraft, getChildBomVersions, updateBomStatus } from '@/api/bom'
|
import { getBomList, getBomSummary, getBomDetail, saveBom, deleteBom, getDraftDetail, saveDraft, publishDraft, getChildBomVersions, updateBomStatus, updateBomArchive } from '@/api/bom'
|
||||||
import ImportDialog from '@/components/ImportDialog.vue'
|
import ImportDialog from '@/components/ImportDialog.vue'
|
||||||
import CompanySelector from '@/components/CompanySelector.vue'
|
import CompanySelector from '@/components/CompanySelector.vue'
|
||||||
import { searchMaterialBase } from '@/api/inbound/buy'
|
import { searchMaterialBase } from '@/api/inbound/buy'
|
||||||
@ -316,6 +319,7 @@ interface BomItem {
|
|||||||
parent_name: string
|
parent_name: string
|
||||||
version: string
|
version: string
|
||||||
is_enabled: boolean
|
is_enabled: boolean
|
||||||
|
is_archived?: boolean
|
||||||
child_count: number
|
child_count: number
|
||||||
}
|
}
|
||||||
interface MaterialBase {
|
interface MaterialBase {
|
||||||
@ -1032,6 +1036,27 @@ const onEnabledStatusChange = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 归档/取消归档:仍启用可编辑,但不再作为其它 BOM 子件的版本候选
|
||||||
|
const handleArchive = async (row: any) => {
|
||||||
|
const target = !row.is_archived
|
||||||
|
const tip = target
|
||||||
|
? `确定归档 ${row.bom_no} (${row.version}) 吗?\n归档后仍可查看/编辑/取消归档,但不再作为其它 BOM 子件的版本候选。`
|
||||||
|
: `确定取消归档 ${row.bom_no} (${row.version}) 吗?`
|
||||||
|
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 || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (e) { ElMessage.error('归档操作失败') }
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = (row: BomItem) => {
|
const handleDelete = (row: BomItem) => {
|
||||||
ElMessageBox.confirm(`确定删除 ${row.bom_no} (${row.version}) 吗?`, '警告', { type: 'warning' })
|
ElMessageBox.confirm(`确定删除 ${row.bom_no} (${row.version}) 吗?`, '警告', { type: 'warning' })
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user