diff --git a/inventory-backend/app/api/v1/outbound.py b/inventory-backend/app/api/v1/outbound.py index ce7e328..1290fc9 100644 --- a/inventory-backend/app/api/v1/outbound.py +++ b/inventory-backend/app/api/v1/outbound.py @@ -199,6 +199,93 @@ def get_outbound_list(): return jsonify({'code': 500, 'msg': str(e)}), 500 +# ============================================================================== +# BOM 匹配库存接口 (POST /api/v1/outbound/bom-match-stock) +# 替代前端 while(true) 全量加载:服务端按 child_ids 精确查询匹配库存 +# ============================================================================== +@outbound_bp.route('/bom-match-stock', methods=['POST']) +@jwt_required() +def bom_match_stock(): + """ + 根据 BOM 子件 base_id 列表,查询三张库存表中有库存的匹配记录。 + + Body: { "child_ids": [1, 2, 3, ...] } + Returns: { "code": 200, "data": { "items": [...] } } + """ + try: + data = request.get_json() or {} + child_ids = data.get('child_ids', []) + if not child_ids: + return jsonify({'code': 400, 'msg': 'child_ids 不能为空'}), 400 + + # 去重 + child_ids = list(set(int(x) for x in child_ids)) + + from app.models.inbound.buy import StockBuy + from app.models.inbound.semi import StockSemi + from app.models.inbound.product import StockProduct + from sqlalchemy.orm import joinedload + + all_items = [] + + # 采购件 + buy_items = StockBuy.query.filter( + StockBuy.base_id.in_(child_ids), + StockBuy.stock_quantity > 0 + ).options(joinedload(StockBuy.base)).all() + for s in buy_items: + d = s.to_dict() + d['type'] = 'material' + d['stock_type'] = 'material' + d['typeLabel'] = '采购件' + d['uniqueKey'] = f"material_{s.id}" + d['name'] = d.get('material_name', '') + d['standard'] = d.get('spec_model', '') + all_items.append(d) + + # 半成品 + try: + semi_items = StockSemi.query.filter( + StockSemi.base_id.in_(child_ids), + StockSemi.stock_quantity > 0 + ).options(joinedload(StockSemi.base)).all() + for s in semi_items: + d = s.to_dict() + d['type'] = 'semi' + d['stock_type'] = 'semi' + d['typeLabel'] = '半成品' + d['uniqueKey'] = f"semi_{s.id}" + d['name'] = d.get('material_name', '') + d['standard'] = d.get('spec_model', '') + all_items.append(d) + except Exception: + pass + + # 成品 + try: + prod_items = StockProduct.query.filter( + StockProduct.base_id.in_(child_ids), + StockProduct.stock_quantity > 0 + ).options(joinedload(StockProduct.base)).all() + for s in prod_items: + d = s.to_dict() + d['type'] = 'product' + d['stock_type'] = 'product' + d['typeLabel'] = '成品' + d['uniqueKey'] = f"product_{s.id}" + d['name'] = d.get('material_name', '') + d['standard'] = d.get('spec_model', '') + all_items.append(d) + except Exception: + pass + + return jsonify({'code': 200, 'msg': 'success', 'data': {'items': all_items}}) + + except Exception as e: + traceback.print_exc() + return jsonify({'code': 500, 'msg': str(e)}), 500 + + # ============================================================================== # 出库审批相关接口 # ============================================================================== diff --git a/inventory-web/src/api/outbound.ts b/inventory-web/src/api/outbound.ts index 220142b..29149db 100644 --- a/inventory-web/src/api/outbound.ts +++ b/inventory-web/src/api/outbound.ts @@ -122,4 +122,16 @@ export function approveRequest(id: number, data: { action: 'approve' | 'reject'; method: 'patch', data }) +} + +/** + * BOM 匹配库存(替代前端 while(true) 全量加载) + * 根据 BOM 子件的 base_id 列表,服务端精确查询有库存的记录 + */ +export function bomMatchStock(childIds: number[]) { + return request({ + url: '/v1/outbound/bom-match-stock', + method: 'post', + data: { child_ids: childIds } + }) } \ No newline at end of file diff --git a/inventory-web/src/views/borrow/apply/index.vue b/inventory-web/src/views/borrow/apply/index.vue index 6f55033..fff50f7 100644 --- a/inventory-web/src/views/borrow/apply/index.vue +++ b/inventory-web/src/views/borrow/apply/index.vue @@ -443,6 +443,7 @@ import { ElMessage, ElTable, ElMessageBox } from 'element-plus' import { printSelectionList } from '@/api/inbound/stock' import { useUserStore } from '@/stores/user' import { submitBorrowRequest, getBorrowStockList } from '@/api/transaction' +import { bomMatchStock } from '@/api/outbound' import { getApproversList } from '@/api/auth' import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom' @@ -602,32 +603,20 @@ const loadStockList = async () => { } } -// BOM 匹配用:全量加载库存(不分页),确保所有物料都能匹配到 -const loadAllStockForBom = async () => { +// BOM 匹配用:服务端精确查询匹配库存(替代前端 while(true) 全量加载) +const loadStockForBom = async (childIds: number[]) => { stockLoading.value = true try { - let allItems: any[] = [] - let page = 1 - const pageSize = 200 - while (true) { - const res: any = await getBorrowStockList({ - page, - pageSize, - is_aggregated: true - }) - const list = (res.data?.list || []).map((item: any) => ({ - ...item, - uniqueKey: `${item.type}_${item.id}`, - warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || '' - })) - allItems = allItems.concat(list) - if (list.length < pageSize) break - page++ - } - stockList.value = allItems - stockTotal.value = allItems.length + const res: any = await bomMatchStock(childIds) + const items = (res.data?.items || []).map((item: any) => ({ + ...item, + uniqueKey: item.uniqueKey || `${item.type}_${item.id}`, + warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || '' + })) + stockList.value = items + stockTotal.value = items.length } catch (e) { - ElMessage.error('加载库存列表失败') + ElMessage.error('加载 BOM 匹配库存失败') } finally { stockLoading.value = false } @@ -743,10 +732,6 @@ watch(selectedBomNo, async (newBomNo) => { const confirmBomAdd = async () => { if (!selectedBomNo.value) return ElMessage.warning('请选择 BOM') - if (stockList.value.length === 0) { - await loadAllStockForBom() - } - if (currentBomDetail.value.length === 0) { try { const detailRes: any = await getBomWithStock(selectedBomNo.value) @@ -758,6 +743,10 @@ const confirmBomAdd = async () => { } const bomRows = currentBomDetail.value + + // ★ 服务端精确查询 BOM 子件匹配库存(替代前端 while(true) 全量加载) + const childIds = bomRows.map((b: any) => b.child_id).filter(Boolean) + await loadStockForBom(childIds) let addedCount = 0 let skippedCount = 0 diff --git a/inventory-web/src/views/outbound/Selection.vue b/inventory-web/src/views/outbound/Selection.vue index 2bc2ee8..0dc96a3 100644 --- a/inventory-web/src/views/outbound/Selection.vue +++ b/inventory-web/src/views/outbound/Selection.vue @@ -442,7 +442,7 @@ import { ElMessage, ElTable, ElMessageBox } from 'element-plus' import { getStockList, printSelectionList } from '@/api/inbound/stock' import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom' import { useUserStore } from '@/stores/user' -import { submitOutboundRequest } from '@/api/outbound' +import { submitOutboundRequest, bomMatchStock } from '@/api/outbound' import { getApproversList } from '@/api/auth' const userStore = useUserStore() @@ -605,32 +605,20 @@ const loadStockList = async () => { } } -// BOM 匹配用:全量加载库存(不分页),确保所有物料都能匹配到 -const loadAllStockForBom = async () => { +// BOM 匹配用:服务端精确查询匹配库存(替代前端 while(true) 全量加载) +const loadStockForBom = async (childIds: number[]) => { stockLoading.value = true try { - let allItems: any[] = [] - let page = 1 - const pageSize = 200 - while (true) { - const res: any = await getStockList({ - page, - pageSize, - is_aggregated: true - }) - const list = (res.data?.list || []).map((item: any) => ({ - ...item, - uniqueKey: `${item.type}_${item.id}`, - warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || '' - })) - allItems = allItems.concat(list) - if (list.length < pageSize) break - page++ - } - stockList.value = allItems - stockTotal.value = allItems.length + const res: any = await bomMatchStock(childIds) + const items = (res.data?.items || []).map((item: any) => ({ + ...item, + uniqueKey: item.uniqueKey || `${item.type}_${item.id}`, + warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || '' + })) + stockList.value = items + stockTotal.value = items.length } catch (e) { - ElMessage.error('加载库存列表失败') + ElMessage.error('加载 BOM 匹配库存失败') } finally { stockLoading.value = false } @@ -776,10 +764,6 @@ watch(selectedBomNo, async (newBomNo) => { const confirmBomAdd = async () => { if (!selectedBomNo.value) return ElMessage.warning('请选择 BOM') - if (stockList.value.length === 0) { - await loadAllStockForBom() - } - if (currentBomDetail.value.length === 0) { try { const detailRes: any = await getBomWithStock(selectedBomNo.value) @@ -791,6 +775,10 @@ const confirmBomAdd = async () => { } const bomRows = currentBomDetail.value + + // ★ 服务端精确查询 BOM 子件匹配库存(替代前端 while(true) 全量加载) + const childIds = bomRows.map((b: any) => b.child_id).filter(Boolean) + await loadStockForBom(childIds) let addedCount = 0 let skippedCount = 0