新增 /cascade-inventory 级联库存缺口查询接口,供 AI 调用 BOM 出库缺口分析
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
Reference in New Issue
Block a user