import uuid from datetime import datetime from sqlalchemy import or_ from app.extensions import db from app.models.outbound import TransOutbound # 导入所有库存实体模型,用于查找和扣减 from app.models.inbound.buy import StockBuy from app.models.inbound.semi import StockSemi from app.models.inbound.product import StockProduct class OutboundService: @staticmethod def generate_outbound_no(): """生成出库单号: OUT-yyyyMMdd-随机码""" date_str = datetime.now().strftime('%Y%m%d') short_uuid = uuid.uuid4().hex[:6].upper() return f"OUT-{date_str}-{short_uuid}" @staticmethod def get_stock_by_barcode(barcode): """ 根据条码在各个库存表中查找 优先级: 成品 -> 半成品 -> 采购件 """ if not barcode: return None # 1. 查成品 prod = StockProduct.query.filter_by(barcode=barcode).first() if prod: return OutboundService._format_scan_result(prod, 'stock_product', prod.sku) # 2. 查半成品 semi = StockSemi.query.filter_by(barcode=barcode).first() if semi: return OutboundService._format_scan_result(semi, 'stock_semi', semi.sku) # 3. 查采购件 buy = StockBuy.query.filter_by(barcode=barcode).first() if buy: # 采购件可能需要关联 material_base 获取名称,这里假设 base_id 关联已建立 name = buy.base.name if buy.base else "未知采购件" spec = buy.base.spec_model if buy.base else "" return OutboundService._format_scan_result(buy, 'stock_buy', buy.sku, name, spec) return None @staticmethod def _format_scan_result(item, table_name, sku, name=None, spec=None): """格式化返回给前端的数据结构""" # 如果没有传 name (例如成品/半成品),尝试通过关联获取,或者直接用 SKU 代替 item_name = name item_spec = spec if not item_name and hasattr(item, 'base') and item.base: item_name = item.base.name item_spec = item.base.spec_model return { 'id': item.id, 'sku': sku, 'name': item_name or sku, 'spec_model': item_spec or '', 'source_table': table_name, 'stock_quantity': float(item.stock_quantity), 'available_quantity': float(item.available_quantity), 'batch_number': getattr(item, 'batch_number', ''), 'warehouse_location': getattr(item, 'warehouse_location', '') } @staticmethod def create_outbound(data, operator_name='System'): """执行出库逻辑:扣减库存 + 记录日志""" source_table = data.get('source_table') stock_id = data.get('stock_id') quantity = float(data.get('quantity', 0)) if quantity <= 0: raise ValueError("出库数量必须大于0") # 1. 获取对应的库存记录模型 model_map = { 'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct } ModelClass = model_map.get(source_table) if not ModelClass: raise ValueError(f"未知的库存来源表: {source_table}") # 2. 锁定并查询库存 (使用 with_for_update 防止并发扣减) stock_item = ModelClass.query.with_for_update().get(stock_id) if not stock_item: raise ValueError("库存记录不存在") if stock_item.available_quantity < quantity: raise ValueError(f"库存不足!当前可用: {stock_item.available_quantity}, 请求出库: {quantity}") try: # 3. 扣减库存 stock_item.stock_quantity -= quantity stock_item.available_quantity -= quantity # 4. 创建出库记录 new_outbound = TransOutbound( outbound_no=OutboundService.generate_outbound_no(), sku=data.get('sku'), source_table=source_table, stock_id=stock_id, barcode=data.get('barcode'), outbound_type=data.get('outbound_type', 'SALES'), quantity=quantity, consumer_name=data.get('consumer_name'), signature_path=data.get('signature_path'), # 存储签名的 URL operator_name=operator_name, remark=data.get('remark') ) db.session.add(new_outbound) db.session.commit() return new_outbound except Exception as e: db.session.rollback() raise e @staticmethod def get_list(page=1, per_page=10, keyword=None, start_date=None, end_date=None): query = TransOutbound.query.order_by(TransOutbound.outbound_time.desc()) if keyword: query = query.filter(or_( TransOutbound.outbound_no.ilike(f'%{keyword}%'), TransOutbound.consumer_name.ilike(f'%{keyword}%'), TransOutbound.sku.ilike(f'%{keyword}%') )) if start_date and end_date: # 假设传入的是 'YYYY-MM-DD',需要处理时间范围 query = query.filter(TransOutbound.outbound_time.between(start_date, end_date)) pagination = query.paginate(page=page, per_page=per_page, error_out=False) return { 'items': [item.to_dict() for item in pagination.items], 'total': pagination.total, 'pages': pagination.pages, 'current_page': page }