新增 /cascade-inventory 级联库存缺口查询接口,供 AI 调用 BOM 出库缺口分析

This commit is contained in:
DXC
2026-06-01 09:59:48 +08:00
parent 992e08aee9
commit f18dfd9819
2 changed files with 106 additions and 1 deletions

View File

@ -382,3 +382,41 @@ def get_bom_parents():
except Exception as e: except Exception as e:
current_app.logger.error(f'获取BOM父件列表失败: {str(e)}') current_app.logger.error(f'获取BOM父件列表失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500 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

View File

@ -2,6 +2,8 @@ from app.extensions import db
from app.models.bom import BomTable from app.models.bom import BomTable
from app.models.base import MaterialBase from app.models.base import MaterialBase
from app.models.inbound.buy import StockBuy 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 sqlalchemy import func, distinct, or_, case
from collections import defaultdict from collections import defaultdict
import uuid import uuid
@ -457,4 +459,69 @@ class BomService:
bom_no = BomService.get_bom_no_by_parent(parent_id) bom_no = BomService.get_bom_no_by_parent(parent_id)
if not bom_no: return [] if not bom_no: return []
detail = BomService.get_bom_with_stock_by_bom_no(bom_no) detail = BomService.get_bom_with_stock_by_bom_no(bom_no)
return detail['children'] if detail else [] 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