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 import uuid from datetime import datetime class BomService: # ====================== 新版 BOM 逻辑(基于 bom_no) ====================== @staticmethod def generate_bom_no(): """生成唯一的 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(): """ 获取所有 BOM 配方(按 bom_no 分组) 返回:每个 BOM 的编号、父件信息、版本、子件数量 """ subq = 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() query = db.session.query( subq.c.bom_no, subq.c.parent_id, subq.c.version, subq.c.child_count, MaterialBase.name.label('parent_name') ).join(MaterialBase, subq.c.parent_id == MaterialBase.id) results = query.all() return [{ 'bom_no': row.bom_no, 'parent_id': row.parent_id, 'parent_name': row.parent_name, 'version': row.version, 'child_count': row.child_count } for row in results] @staticmethod def get_bom_detail(bom_no): """ 根据 bom_no 获取配方详情 返回包含父件信息和子件列表的对象 """ rows = db.session.query( BomTable, MaterialBase.name.label('child_name') ).join( MaterialBase, BomTable.child_id == MaterialBase.id ).filter( BomTable.bom_no == bom_no ).all() if not rows: return None first = rows[0] parent_id = first.BomTable.parent_id parent_name = db.session.query(MaterialBase.name)\ .filter(MaterialBase.id == parent_id).scalar() or '' children = [] for bom, child_name in rows: children.append({ 'child_id': bom.child_id, 'child_name': child_name, 'dosage': float(bom.dosage) if bom.dosage else 0.0, 'remark': bom.remark or '' }) return { 'bom_no': bom_no, 'parent_id': parent_id, 'parent_name': parent_name, 'version': first.BomTable.version, 'children': children } @staticmethod def save_bom(data): """ 保存或更新一个 BOM 配方 data 结构: { "bom_no": "可选,为空则新建", "version": "版本号,默认v1", "parent_id": 父件ID, "children": [ {"child_id": 1, "dosage": 2.5, "remark": ""}, ... ] } """ bom_no = data.get('bom_no') version = data.get('version', 'v1') parent_id = data['parent_id'] children = data['children'] # 校验父件不能与子件相同 for child in children: if child['child_id'] == parent_id: raise ValueError('父件与子件不能是同一物料') # 如果未提供 bom_no,则生成一个新的 if not bom_no: bom_no = BomService.generate_bom_no() else: # 删除该 bom_no 下所有现有记录 BomTable.query.filter_by(bom_no=bom_no).delete() # 插入新记录 for child in children: bom = BomTable( bom_no=bom_no, version=version, parent_id=parent_id, child_id=child['child_id'], dosage=child.get('dosage', 0), remark=child.get('remark', '') ) db.session.add(bom) db.session.commit() return bom_no @staticmethod def get_bom_with_stock_by_bom_no(bom_no): """ 根据 bom_no 获取配方详情,并计算每个子件的库存和最大可生产数量 """ detail = BomService.get_bom_detail(bom_no) if not detail: return None for child in detail['children']: # 查询该子件在 StockBuy 中的可用库存总量 stock_qty = db.session.query( func.coalesce(func.sum(StockBuy.available_quantity), 0) ).filter( StockBuy.base_id == child['child_id'] ).scalar() or 0 child['current_stock'] = float(stock_qty) dosage = child['dosage'] child['max_producible'] = int(stock_qty // dosage) if dosage > 0 else 0 return detail # ====================== 兼容旧接口(基于 parent_id) ====================== @staticmethod def get_bom_no_by_parent(parent_id): """ 根据父件 ID 获取其最新的 BOM 编号(用于兼容旧接口) """ row = BomTable.query.filter_by(parent_id=parent_id)\ .order_by(BomTable.version.desc()).first() return row.bom_no if row else None @staticmethod def create_or_update_bom(parent_id, child_list, bom_no=None, version='v1'): """ 兼容旧接口的保存方法(保留原有调用方式) 如果提供了 bom_no,则更新该 bom_no;否则为父件创建新 BOM。 """ # 校验父件不能与子件相同 for item in child_list: if item['child_id'] == parent_id: raise ValueError('父件与子件不能是同一物料') # 如果未提供 bom_no,尝试查找现有的,否则新建 if not bom_no: existing = BomTable.query.filter_by(parent_id=parent_id).first() bom_no = existing.bom_no if existing else BomService.generate_bom_no() # 删除该 bom_no 下所有记录 BomTable.query.filter_by(bom_no=bom_no).delete() # 插入新的 for item in child_list: bom = BomTable( bom_no=bom_no, version=version, parent_id=parent_id, child_id=item['child_id'], dosage=item.get('dosage', 0), remark=item.get('remark', '') ) db.session.add(bom) db.session.commit() return True @staticmethod def get_bom_with_stock(parent_id): """ 兼容旧接口:根据父件 ID 获取 BOM 及库存信息 (实际会找到对应的 bom_no 再调用新方法) """ 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) if not detail: return [] return detail['children']