From f18dfd981988316849449f377b301b39f578df4b Mon Sep 17 00:00:00 2001 From: DXC Date: Mon, 1 Jun 2026 09:59:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20/cascade-inventory=20?= =?UTF-8?q?=E7=BA=A7=E8=81=94=E5=BA=93=E5=AD=98=E7=BC=BA=E5=8F=A3=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=8E=A5=E5=8F=A3=EF=BC=8C=E4=BE=9B=20AI=20=E8=B0=83?= =?UTF-8?q?=E7=94=A8=20BOM=20=E5=87=BA=E5=BA=93=E7=BC=BA=E5=8F=A3=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inventory-backend/app/api/v1/bom.py | 38 ++++++++++ inventory-backend/app/services/bom_service.py | 69 ++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/inventory-backend/app/api/v1/bom.py b/inventory-backend/app/api/v1/bom.py index 6c6ff23..b2c2832 100644 --- a/inventory-backend/app/api/v1/bom.py +++ b/inventory-backend/app/api/v1/bom.py @@ -382,3 +382,41 @@ def get_bom_parents(): except Exception as e: current_app.logger.error(f'获取BOM父件列表失败: {str(e)}') return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 + + +@bom_bp.route('/cascade-inventory', methods=['GET']) +@jwt_required() +@permission_required('bom_manage') +def get_cascade_inventory(): + """ + 根据 BOM 编号和订单数量,计算所有子件的级联库存缺口(供 AI 调用) + Query参数: + - bom_no: BOM编号(必填) + - order_qty: 订单需求量(必填,数值) + """ + try: + bom_no = request.args.get('bom_no', '').strip() + order_qty_str = request.args.get('order_qty', '').strip() + + if not bom_no: + return jsonify({'code': 400, 'msg': 'bom_no 不能为空'}), 400 + if not order_qty_str: + return jsonify({'code': 400, 'msg': 'order_qty 不能为空'}), 400 + + try: + order_qty = float(order_qty_str) + except ValueError: + return jsonify({'code': 400, 'msg': 'order_qty 必须为有效数字'}), 400 + + data = BomService.calculate_cascade_inventory(bom_no, order_qty) + if data is None: + return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404 + + return jsonify({ + 'code': 200, + 'msg': 'success', + 'data': data + }) + except Exception as e: + current_app.logger.error(f'级联库存计算失败: {str(e)}') + return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 diff --git a/inventory-backend/app/services/bom_service.py b/inventory-backend/app/services/bom_service.py index 0047e56..db2d857 100644 --- a/inventory-backend/app/services/bom_service.py +++ b/inventory-backend/app/services/bom_service.py @@ -2,6 +2,8 @@ from app.extensions import db from app.models.bom import BomTable from app.models.base import MaterialBase from app.models.inbound.buy import StockBuy +from app.models.inbound.semi import StockSemi +from app.models.inbound.product import StockProduct from sqlalchemy import func, distinct, or_, case from collections import defaultdict import uuid @@ -457,4 +459,69 @@ class BomService: bom_no = BomService.get_bom_no_by_parent(parent_id) if not bom_no: return [] detail = BomService.get_bom_with_stock_by_bom_no(bom_no) - return detail['children'] if detail else [] \ No newline at end of file + return detail['children'] if detail else [] + + @staticmethod + def calculate_cascade_inventory(bom_no, order_qty): + """ + 根据 bom_no 和订单数量,计算所有子件的级联库存缺口。 + 返回结构供 AI 消费,每个子件包含:parent_name / spec / name / level_type / + need_qty / available_stock / suggested_qty / gap + 若 BOM 不存在返回 None。 + """ + # 1. 获取 BOM 明细 + detail = BomService.get_bom_detail(bom_no) + if not detail or not detail.get('children'): + return None + + parent_name = detail.get('parent_name', '') + + # 2. 提取所有子件 ID,查询采购库存(stock_buy) + child_ids = [child['child_id'] for child in detail['children']] + + buy_stats = db.session.query( + StockBuy.base_id, + func.coalesce(func.sum(StockBuy.available_quantity), 0).label('total_qty') + ).filter( + StockBuy.base_id.in_(child_ids) + ).group_by(StockBuy.base_id).all() + + buy_map = {stat.base_id: float(stat.total_qty) for stat in buy_stats} + + # 3. 提取所有子件的基础物料信息(名称/规格/类型) + materials = db.session.query( + MaterialBase.id, + MaterialBase.name, + MaterialBase.spec_model + ).filter(MaterialBase.id.in_(child_ids)).all() + + mat_map = { + m.id: {'name': m.name or '', 'spec': m.spec_model or ''} + for m in materials + } + + # 4. 遍历子件,计算每个子件的缺口数据 + results = [] + for child in detail['children']: + child_id = child['child_id'] + dosage = float(child.get('dosage') or 0) + need_qty = dosage * order_qty + + available_stock = buy_map.get(child_id, 0) + suggested_qty = max(0.0, min(need_qty, available_stock)) + gap = available_stock - need_qty + + mat_info = mat_map.get(child_id, {'name': '', 'spec': ''}) + + results.append({ + 'parent_name': parent_name, + 'spec': mat_info['spec'], + 'name': mat_info['name'], + 'level_type': 'child', + 'need_qty': round(need_qty, 4), + 'available_stock': round(available_stock, 4), + 'suggested_qty': round(suggested_qty, 4), + 'gap': round(gap, 4), + }) + + return results \ No newline at end of file