修改bom表逻辑和出库选单内容

This commit is contained in:
dxc
2026-02-12 10:39:21 +08:00
parent b93a565c82
commit d61668bc4b
8 changed files with 838 additions and 296 deletions

View File

@ -8,14 +8,16 @@ from sqlalchemy import distinct
bom_bp = Blueprint('bom', __name__)
# ==================== 新版 BOM 接口(基于 bom_no ====================
@bom_bp.route('/list', methods=['GET'])
@jwt_required()
def get_bom_list():
"""获取所有 BOM 配方列表(按 bom_no 分组)"""
"""获取所有 BOM 配方列表(按 bom_no 分组),支持 keyword 搜索"""
try:
data = BomService.get_bom_list()
keyword = request.args.get('keyword', '').strip()
data = BomService.get_bom_list(keyword=keyword)
return jsonify({
'code': 200,
'msg': 'success',
@ -25,6 +27,7 @@ def get_bom_list():
current_app.logger.error(f'获取BOM列表失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@bom_bp.route('/detail/<bom_no>', methods=['GET'])
@jwt_required()
def get_bom_detail(bom_no):
@ -42,16 +45,21 @@ def get_bom_detail(bom_no):
current_app.logger.error(f'获取BOM详情失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@bom_bp.route('/save', methods=['POST'])
@jwt_required()
def save_bom():
"""保存或更新 BOM 配方(支持新建和另存为新版本"""
"""保存或更新 BOM 配方(支持自定义 bom_no"""
try:
req_data = request.get_json()
# 必需字段校验
if 'parent_id' not in req_data or 'children' not in req_data:
return jsonify({'code': 400, 'msg': '缺少 parent_id 或 children 字段'}), 400
# 校验 bom_no 不能为空(如果前端要求必须填)
if 'bom_no' in req_data and not req_data['bom_no']:
return jsonify({'code': 400, 'msg': 'BOM编号不能为空'}), 400
bom_no = BomService.save_bom(req_data)
return jsonify({
'code': 200,
@ -64,6 +72,7 @@ def save_bom():
current_app.logger.error(f'保存BOM失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@bom_bp.route('/stock/<bom_no>', methods=['GET'])
@jwt_required()
def get_bom_with_stock_by_no(bom_no):
@ -81,6 +90,7 @@ def get_bom_with_stock_by_no(bom_no):
current_app.logger.error(f'获取BOM库存信息失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
# ==================== 删除BOM接口根据bom_no删除整个配方 ====================
@bom_bp.route('/<bom_no>', methods=['DELETE'])
@ -92,7 +102,7 @@ def delete_bom(bom_no):
exist = BomTable.query.filter_by(bom_no=bom_no).first()
if not exist:
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
# 删除该 bom_no 下所有记录
BomTable.query.filter_by(bom_no=bom_no).delete()
db.session.commit()
@ -104,6 +114,7 @@ def delete_bom(bom_no):
current_app.logger.error(f'删除BOM失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
# ==================== 兼容旧接口(保留不改动现有前端) ====================
@bom_bp.route('/<int:parent_id>', methods=['GET'])
@ -120,6 +131,7 @@ def get_bom(parent_id):
current_app.logger.error(f'获取BOM失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@bom_bp.route('', methods=['POST'])
@jwt_required()
def save_bom_legacy():
@ -140,6 +152,7 @@ def save_bom_legacy():
current_app.logger.error(f'保存BOM失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@bom_bp.route('/base/list', methods=['GET'])
@jwt_required()
def get_material_base_list():
@ -156,6 +169,7 @@ def get_material_base_list():
current_app.logger.error(f'获取基础物料列表失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@bom_bp.route('/parents', methods=['GET'])
@jwt_required()
def get_bom_parents():
@ -171,4 +185,4 @@ def get_bom_parents():
})
except Exception as e:
current_app.logger.error(f'获取BOM父件列表失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500

View File

@ -2,32 +2,76 @@ 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 sqlalchemy import func, distinct
from sqlalchemy import func, distinct, or_
import uuid
from datetime import datetime
class BomService:
# ====================== 新版 BOM 逻辑(基于 bom_no ======================
@staticmethod
def generate_bom_no():
"""生成唯一的 BOM 编号"""
"""生成唯一的 BOM 编号 (作为默认备选)"""
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
unique = str(uuid.uuid4())[:8]
return f'BOM-{timestamp}-{unique}'
@staticmethod
def get_bom_list():
def get_bom_list(keyword=None):
"""
获取所有 BOM 配方(按 bom_no 分组)
返回:每个 BOM编号、父件信息、版本、子件数量
支持模糊搜索:BOM编号、父件名称/规格、子件名称/规格
"""
subq = db.session.query(
# 1. 如果有搜索关键词,先筛选出符合条件的 bom_no 集合
filtered_bom_nos = None
if keyword:
kw = f'%{keyword}%'
# 条件A: 匹配 BOM编号 或 父件信息
# 需要 join 父件表
q1 = db.session.query(BomTable.bom_no).join(
MaterialBase, BomTable.parent_id == MaterialBase.id
).filter(
or_(
BomTable.bom_no.ilike(kw),
MaterialBase.name.ilike(kw),
MaterialBase.spec_model.ilike(kw)
)
)
# 条件B: 匹配 子件信息
# 需要 join 子件表
q2 = db.session.query(BomTable.bom_no).join(
MaterialBase, BomTable.child_id == MaterialBase.id
).filter(
or_(
MaterialBase.name.ilike(kw),
MaterialBase.spec_model.ilike(kw)
)
)
# 取并集 (Union)
filtered_bom_nos = q1.union(q2).distinct().all()
filtered_bom_nos = [row[0] for row in filtered_bom_nos]
# 如果搜不到任何结果,直接返回空
if not filtered_bom_nos:
return []
# 2. 原有的分组聚合查询
subq_query = db.session.query(
BomTable.bom_no,
BomTable.parent_id,
BomTable.version,
func.count(BomTable.child_id).label('child_count')
).group_by(BomTable.bom_no, BomTable.parent_id, BomTable.version).subquery()
)
# 应用筛选
if filtered_bom_nos is not None:
subq_query = subq_query.filter(BomTable.bom_no.in_(filtered_bom_nos))
subq = subq_query.group_by(BomTable.bom_no, BomTable.parent_id, BomTable.version).subquery()
query = db.session.query(
subq.c.bom_no,
@ -38,6 +82,9 @@ class BomService:
MaterialBase.spec_model.label('parent_spec')
).join(MaterialBase, subq.c.parent_id == MaterialBase.id)
# 按 bom_no 倒序排列
query = query.order_by(subq.c.bom_no.desc())
results = query.all()
return [{
'bom_no': row.bom_no,
@ -103,13 +150,10 @@ class BomService:
保存或更新一个 BOM 配方
data 结构:
{
"bom_no": "可选,为空则新建",
"bom_no": "用户输入或自动生成",
"version": "版本号默认v1",
"parent_id": 父件ID,
"children": [
{"child_id": 1, "dosage": 2.5, "remark": ""},
...
]
"children": [...]
}
"""
bom_no = data.get('bom_no')
@ -122,12 +166,14 @@ class BomService:
if child['child_id'] == parent_id:
raise ValueError('父件与子件不能是同一物料')
# 如果未提供 bom_no则生成一个新的
if not bom_no:
# 如果未提供 bom_no则生成一个新的 (兼容旧逻辑,但现在前端会传值)
if not bom_no or not bom_no.strip():
# 如果前端没传,抛出异常要求用户填写,或者自动生成
# 这里选择自动生成作为兜底,但推荐前端校验必填
bom_no = BomService.generate_bom_no()
else:
# 删除该 bom_no 下所有现有记录
BomTable.query.filter_by(bom_no=bom_no).delete()
# 删除该 bom_no 下所有现有记录 (全量更新模式)
BomTable.query.filter_by(bom_no=bom_no).delete()
# 插入新记录
for child in children:
@ -174,7 +220,7 @@ class BomService:
"""
根据父件 ID 获取其最新的 BOM 编号(用于兼容旧接口)
"""
row = BomTable.query.filter_by(parent_id=parent_id)\
row = BomTable.query.filter_by(parent_id=parent_id) \
.order_by(BomTable.version.desc()).first()
return row.bom_no if row else None
@ -224,4 +270,4 @@ class BomService:
detail = BomService.get_bom_with_stock_by_bom_no(bom_no)
if not detail:
return []
return detail['children']
return detail['children']