出库操作逻辑上面实现,成功跑通
This commit is contained in:
@ -1,12 +1,13 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.outbound_service import OutboundService
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
import traceback
|
||||
|
||||
outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound')
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 1. 扫码查询库存接口
|
||||
# 1. 扫码查询库存接口 (关联三个库存表)
|
||||
# GET /api/v1/outbound/scan?barcode=...
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('/scan', methods=['GET'])
|
||||
@ -17,13 +18,24 @@ def scan_barcode():
|
||||
return jsonify({'code': 400, 'msg': '请提供条码'}), 400
|
||||
|
||||
try:
|
||||
# 调用 Service 层去三个表中查找
|
||||
result = OutboundService.get_stock_by_barcode(barcode)
|
||||
|
||||
if result:
|
||||
return jsonify({'code': 200, 'data': result, 'msg': '扫描成功'})
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '扫描成功',
|
||||
'data': result
|
||||
})
|
||||
else:
|
||||
return jsonify({'code': 404, 'msg': '未找到对应的库存记录'}), 404
|
||||
return jsonify({
|
||||
'code': 404,
|
||||
'msg': '未找到对应的库存记录,请确认条码是否正确'
|
||||
}), 404
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'扫描查询出错: {str(e)}'}), 500
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
@ -37,24 +49,37 @@ def create_outbound():
|
||||
if not data:
|
||||
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
||||
|
||||
current_user = get_jwt_identity() # 获取当前登录用户作为操作员
|
||||
# 获取当前登录用户名 (JWT identity)
|
||||
current_user_name = get_jwt_identity()
|
||||
if not current_user_name:
|
||||
current_user_name = 'Unknown'
|
||||
|
||||
# 简单的必填校验 (更复杂的校验可放入 Schema)
|
||||
# ★ [修改] 获取最终的操作员名称
|
||||
# 优先取前端传来的 operator_name (你在前端下拉框选的人)
|
||||
# 如果前端没传,则回退使用当前登录用户的名字
|
||||
final_operator = data.get('operator_name')
|
||||
if not final_operator:
|
||||
final_operator = current_user_name
|
||||
|
||||
# 必填校验
|
||||
required_fields = ['stock_id', 'source_table', 'quantity', 'consumer_name', 'signature_path']
|
||||
for field in required_fields:
|
||||
if field not in data or not data[field]:
|
||||
return jsonify({'code': 400, 'msg': f'缺少必填字段: {field}'}), 400
|
||||
|
||||
try:
|
||||
outbound_record = OutboundService.create_outbound(data, operator_name=current_user)
|
||||
# ★ [修改] 将确认后的操作员名称传给 Service
|
||||
outbound_record = OutboundService.create_outbound(data, operator_name=final_operator)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '出库成功',
|
||||
'data': outbound_record.to_dict()
|
||||
})
|
||||
except ValueError as e:
|
||||
# 业务逻辑错误 (如库存不足)
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||
|
||||
|
||||
@ -67,11 +92,10 @@ def create_outbound():
|
||||
def get_outbound_list():
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = int(request.args.get('limit', 10))
|
||||
limit = int(request.args.get('limit', 10))
|
||||
keyword = request.args.get('keyword', '')
|
||||
# 日期范围处理可根据前端传参格式调整
|
||||
|
||||
result = OutboundService.get_list(page, per_page, keyword)
|
||||
result = OutboundService.get_list(page, limit, keyword)
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
@ -79,4 +103,5 @@ def get_outbound_list():
|
||||
'data': result
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
@ -10,17 +10,17 @@ class TransOutbound(db.Model):
|
||||
|
||||
# 关联源库存信息
|
||||
sku = db.Column(db.String(100))
|
||||
source_table = db.Column(db.String(50)) # 'stock_buy', 'stock_product', etc.
|
||||
source_table = db.Column(db.String(50)) # 'stock_buy', 'stock_product', 'stock_semi'
|
||||
stock_id = db.Column(db.Integer) # 对应源表的主键ID
|
||||
barcode = db.Column(db.String(100)) # 实际扫码内容
|
||||
|
||||
# 业务信息
|
||||
outbound_type = db.Column(db.String(50), default='SALES') # SALES, USE, TRANSFER
|
||||
outbound_type = db.Column(db.String(50), default='SALES') # SALES(销售), USE(领用), TRANSFER(调拨)
|
||||
quantity = db.Column(db.Numeric(19, 4), nullable=False)
|
||||
|
||||
# 签字与追溯
|
||||
consumer_name = db.Column(db.String(100)) # 领用人/客户
|
||||
signature_path = db.Column(db.Text) # 签名图片路径
|
||||
signature_path = db.Column(db.Text) # 电子签名图片路径
|
||||
outbound_time = db.Column(db.DateTime, default=datetime.now)
|
||||
operator_name = db.Column(db.String(100)) # 操作员
|
||||
|
||||
|
||||
@ -4,10 +4,12 @@ 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
|
||||
# ★★★ [关键] 引入基础信息表,用于手动补全名称
|
||||
from app.models.base import MaterialBase
|
||||
|
||||
|
||||
class OutboundService:
|
||||
@ -22,58 +24,92 @@ class OutboundService:
|
||||
@staticmethod
|
||||
def get_stock_by_barcode(barcode):
|
||||
"""
|
||||
根据条码在各个库存表中查找
|
||||
优先级: 成品 -> 半成品 -> 采购件
|
||||
[核心逻辑] 根据扫码内容查找对应的库存物品
|
||||
查找顺序: 成品 (StockProduct) -> 半成品 (StockSemi) -> 采购件 (StockBuy)
|
||||
匹配逻辑: 匹配 barcode 字段 OR sku 字段
|
||||
"""
|
||||
if not barcode:
|
||||
return None
|
||||
|
||||
# 1. 查成品
|
||||
prod = StockProduct.query.filter_by(barcode=barcode).first()
|
||||
clean_code = barcode.strip()
|
||||
|
||||
# --- 1. 查找成品表 (StockProduct) ---
|
||||
prod = StockProduct.query.filter(
|
||||
or_(StockProduct.barcode == clean_code, StockProduct.sku == clean_code)
|
||||
).first()
|
||||
if prod:
|
||||
return OutboundService._format_scan_result(prod, 'stock_product', prod.sku)
|
||||
return OutboundService._format_scan_result(prod, 'stock_product')
|
||||
|
||||
# 2. 查半成品
|
||||
semi = StockSemi.query.filter_by(barcode=barcode).first()
|
||||
# --- 2. 查找半成品表 (StockSemi) ---
|
||||
semi = StockSemi.query.filter(
|
||||
or_(StockSemi.barcode == clean_code, StockSemi.sku == clean_code)
|
||||
).first()
|
||||
if semi:
|
||||
return OutboundService._format_scan_result(semi, 'stock_semi', semi.sku)
|
||||
return OutboundService._format_scan_result(semi, 'stock_semi')
|
||||
|
||||
# 3. 查采购件
|
||||
buy = StockBuy.query.filter_by(barcode=barcode).first()
|
||||
# --- 3. 查找采购件表 (StockBuy) ---
|
||||
buy = StockBuy.query.filter(
|
||||
or_(StockBuy.barcode == clean_code, StockBuy.sku == clean_code)
|
||||
).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 OutboundService._format_scan_result(buy, 'stock_buy')
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _format_scan_result(item, table_name, sku, name=None, spec=None):
|
||||
"""格式化返回给前端的数据结构"""
|
||||
# 如果没有传 name (例如成品/半成品),尝试通过关联获取,或者直接用 SKU 代替
|
||||
item_name = name
|
||||
item_spec = spec
|
||||
def _format_scan_result(item, table_name):
|
||||
"""
|
||||
[核心修复] 格式化返回数据,确保名称和规格一定能取到
|
||||
"""
|
||||
base_name = ""
|
||||
base_spec = ""
|
||||
|
||||
if not item_name and hasattr(item, 'base') and item.base:
|
||||
item_name = item.base.name
|
||||
item_spec = item.base.spec_model
|
||||
# -------------------------------------------------------
|
||||
# 修复逻辑:强制获取基础信息
|
||||
# -------------------------------------------------------
|
||||
|
||||
# 步骤 1: 尝试通过 ORM 关联获取 (如果有定义 relationship)
|
||||
if hasattr(item, 'base') and item.base:
|
||||
base_name = item.base.name
|
||||
base_spec = item.base.spec_model
|
||||
|
||||
# 步骤 2: [关键] 如果步骤1失败,但有 base_id,则手动查询 MaterialBase 表
|
||||
# 这能解决“扫码有库存但显示未知物品”的问题
|
||||
if not base_name and hasattr(item, 'base_id') and item.base_id:
|
||||
try:
|
||||
# 手动查基础表
|
||||
base_info = MaterialBase.query.get(item.base_id)
|
||||
if base_info:
|
||||
base_name = base_info.name
|
||||
base_spec = base_info.spec_model
|
||||
except Exception as e:
|
||||
print(f"基础信息查询失败: {e}")
|
||||
|
||||
# 步骤 3: 兜底逻辑,某些旧表可能直接存了 material_name 字段
|
||||
if not base_name and hasattr(item, 'material_name'):
|
||||
base_name = item.material_name
|
||||
|
||||
stock_qty = float(item.stock_quantity) if item.stock_quantity else 0
|
||||
avail_qty = float(item.available_quantity) if item.available_quantity else 0
|
||||
|
||||
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),
|
||||
'sku': item.sku,
|
||||
'name': base_name or "未知物品", # 此时应该能正确显示名称了
|
||||
'spec_model': base_spec or "", # 此时应该能正确显示规格了
|
||||
'source_table': table_name, # 标记来源表
|
||||
'stock_quantity': stock_qty, # 当前库存
|
||||
'available_quantity': avail_qty, # 可用库存
|
||||
'batch_number': getattr(item, 'batch_number', ''),
|
||||
'warehouse_location': getattr(item, 'warehouse_location', '')
|
||||
'warehouse_location': getattr(item, 'warehouse_location', ''),
|
||||
'barcode': getattr(item, 'barcode', '')
|
||||
}
|
||||
|
||||
@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))
|
||||
@ -81,7 +117,7 @@ class OutboundService:
|
||||
if quantity <= 0:
|
||||
raise ValueError("出库数量必须大于0")
|
||||
|
||||
# 1. 获取对应的库存记录模型
|
||||
# 1. 动态映射表模型
|
||||
model_map = {
|
||||
'stock_buy': StockBuy,
|
||||
'stock_semi': StockSemi,
|
||||
@ -90,22 +126,24 @@ class OutboundService:
|
||||
|
||||
ModelClass = model_map.get(source_table)
|
||||
if not ModelClass:
|
||||
raise ValueError(f"未知的库存来源表: {source_table}")
|
||||
raise ValueError(f"无效的数据来源表: {source_table}")
|
||||
|
||||
# 2. 锁定并查询库存 (使用 with_for_update 防止并发扣减)
|
||||
# 2. 锁定并查询库存 (使用 with_for_update 防止并发超卖)
|
||||
stock_item = ModelClass.query.with_for_update().get(stock_id)
|
||||
if not stock_item:
|
||||
raise ValueError("库存记录不存在")
|
||||
raise ValueError("库存记录不存在或已被删除")
|
||||
|
||||
if stock_item.available_quantity < quantity:
|
||||
raise ValueError(f"库存不足!当前可用: {stock_item.available_quantity}, 请求出库: {quantity}")
|
||||
# 3. 校验库存充足
|
||||
current_avail = float(stock_item.available_quantity)
|
||||
if current_avail < quantity:
|
||||
raise ValueError(f"库存不足!当前可用: {current_avail}, 请求出库: {quantity}")
|
||||
|
||||
try:
|
||||
# 3. 扣减库存
|
||||
stock_item.stock_quantity -= quantity
|
||||
stock_item.available_quantity -= quantity
|
||||
# 4. 扣减库存
|
||||
stock_item.stock_quantity = float(stock_item.stock_quantity) - quantity
|
||||
stock_item.available_quantity = float(stock_item.available_quantity) - quantity
|
||||
|
||||
# 4. 创建出库记录
|
||||
# 5. 创建出库记录
|
||||
new_outbound = TransOutbound(
|
||||
outbound_no=OutboundService.generate_outbound_no(),
|
||||
sku=data.get('sku'),
|
||||
@ -131,6 +169,7 @@ class OutboundService:
|
||||
|
||||
@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:
|
||||
@ -141,10 +180,10 @@ class OutboundService:
|
||||
))
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user