diff --git a/inventory-backend/app/api/v1/bom.py b/inventory-backend/app/api/v1/bom.py index c5c723c..27e0361 100644 --- a/inventory-backend/app/api/v1/bom.py +++ b/inventory-backend/app/api/v1/bom.py @@ -136,6 +136,35 @@ def update_bom_status(): 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/', methods=['GET']) @jwt_required() @permission_required('bom_manage') diff --git a/inventory-backend/app/services/bom_service.py b/inventory-backend/app/services/bom_service.py index 0d21125..c61e73d 100644 --- a/inventory-backend/app/services/bom_service.py +++ b/inventory-backend/app/services/bom_service.py @@ -133,6 +133,7 @@ class BomService: MaterialBase.spec_model.label('parent_spec'), MaterialBase.category.label('parent_category'), BomTable.is_enabled, + BomTable.is_archived, func.count(BomTable.child_id).label('child_count'), func.string_agg(child_alias.name, ', ').label('child_names'), func.string_agg(child_alias.spec_model, ', ').label('child_specs') @@ -143,14 +144,17 @@ class BomService: ).group_by( BomTable.bom_no, BomTable.version, BomTable.parent_id, 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': - query = query.filter(BomTable.is_enabled == True) + query = query.filter(BomTable.is_enabled == True, BomTable.is_archived == False) elif status == 'disabled': query = query.filter(BomTable.is_enabled == False) + elif status == 'archived': + query = query.filter(BomTable.is_archived == True) elif active_only: query = query.filter(BomTable.is_enabled == True) @@ -211,6 +215,7 @@ class BomService: 'parent_spec': row.parent_spec or '', 'parent_category': row.parent_category or '', 'is_enabled': row.is_enabled, + 'is_archived': row.is_archived, 'child_count': row.child_count, 'child_names': row.child_names or '', 'child_specs': row.child_specs or '' @@ -254,10 +259,13 @@ class BomService: query = query.filter(MaterialBase.company_name == company_limit) # 状态过滤(按 (bom_no, version) 维度计数,故过滤粒度到行即可,行状态同版本一致) + # enabled=启用且未归档;disabled=停用;archived=归档 if status == 'enabled': - query = query.filter(BomTable.is_enabled == True) + query = query.filter(BomTable.is_enabled == True, BomTable.is_archived == False) elif status == 'disabled': query = query.filter(BomTable.is_enabled == False) + elif status == 'archived': + query = query.filter(BomTable.is_archived == True) # 关键词搜索 if keyword: @@ -313,8 +321,12 @@ class BomService: if version: query = query.filter(BomTable.version == version) 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: return None 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}") 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 def get_bom_with_stock_by_bom_no(bom_no, version=None): """ @@ -580,7 +608,12 @@ class BomService: # ====================== 兼容旧接口 ====================== @staticmethod 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 @staticmethod diff --git a/inventory-web/src/api/bom.ts b/inventory-web/src/api/bom.ts index eba398e..75f0cac 100644 --- a/inventory-web/src/api/bom.ts +++ b/inventory-web/src/api/bom.ts @@ -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 export function saveBom(data: any) { return request({ diff --git a/inventory-web/src/views/bom/BomManage.vue b/inventory-web/src/views/bom/BomManage.vue index 72ed5f7..5b01889 100644 --- a/inventory-web/src/views/bom/BomManage.vue +++ b/inventory-web/src/views/bom/BomManage.vue @@ -22,6 +22,7 @@ 全部 启用 停用 + 归档 全部展开 全部折叠 @@ -55,13 +56,15 @@ - + @@ -301,7 +304,7 @@ import { useRoute } from 'vue-router' import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus' import { Plus, Search, EditPen, Upload } from '@element-plus/icons-vue' 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 CompanySelector from '@/components/CompanySelector.vue' import { searchMaterialBase } from '@/api/inbound/buy' @@ -316,6 +319,7 @@ interface BomItem { parent_name: string version: string is_enabled: boolean + is_archived?: boolean child_count: number } 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) => { ElMessageBox.confirm(`确定删除 ${row.bom_no} (${row.version}) 吗?`, '警告', { type: 'warning' }) .then(async () => {