Files
KCGL/inventory-backend/app/api/v1/inbound/stock.py

463 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from flask import Blueprint, jsonify, request
from app.extensions import db
# ★★★ 修复点:必须引入 datetime否则下方更新时间时会报错 500 ★★★
from datetime import datetime
from app.utils.decorators import permission_required
import uuid as uuid_module
# 导入模型
from app.models.inbound.buy import StockBuy
from app.models.inbound.stocktake import StocktakeDraft
from app.models.transaction import TransBorrow
def _normalize_user_id(user_id):
"""规范化 user_id确保是有效字符串"""
if not user_id or not isinstance(user_id, str) or len(user_id) > 100:
return 'admin'
return user_id.strip()
# 尝试导入半成品和成品
try:
from app.models.inbound.semi import StockSemi
except ImportError:
StockSemi = None
try:
from app.models.inbound.product import StockProduct
except ImportError:
StockProduct = None
from app.services.print.network_print_service import NetworkPrintService
bp = Blueprint('stock_ops', __name__)
# ============================================================
# 辅助函数:获取库存记录
# ============================================================
def get_stock_record(source_table, stock_id):
"""根据库存类型和ID获取库存记录"""
if source_table == 'stock_buy' and StockBuy:
return StockBuy.query.get(stock_id)
elif source_table == 'stock_semi' and StockSemi:
return StockSemi.query.get(stock_id)
elif source_table == 'stock_product' and StockProduct:
return StockProduct.query.get(stock_id)
return None
def get_stock_info(uuid_or_barcode):
"""
根据 uuid 或 barcode 查询库存信息
返回: (item, source_table, stock_id)
"""
# 1. 成品
if StockProduct:
item = StockProduct.query.filter(
db.or_(StockProduct.barcode == uuid_or_barcode, StockProduct.sku == uuid_or_barcode)
).first()
if item:
return (item, 'stock_product', item.id)
# 2. 半成品
if StockSemi:
item = StockSemi.query.filter(
db.or_(StockSemi.barcode == uuid_or_barcode, StockSemi.sku == uuid_or_barcode)
).first()
if item:
return (item, 'stock_semi', item.id)
# 3. 采购件
if StockBuy:
item = StockBuy.query.filter(
db.or_(StockBuy.barcode == uuid_or_barcode, StockBuy.sku == uuid_or_barcode)
).first()
if item:
return (item, 'stock_buy', item.id)
return (None, None, None)
@bp.route('/all', methods=['GET'])
@permission_required('inventory_stocktake')
def get_all_stock():
"""
获取所有库存 > 0 的物品
"""
try:
# 1. 采购件
materials = []
if StockBuy:
materials = StockBuy.query.filter(StockBuy.stock_quantity > 0).all()
# 2. 半成品
semis = []
if StockSemi:
try:
semis = StockSemi.query.filter(StockSemi.stock_quantity > 0).all()
except Exception:
semis = []
# 3. 成品
products = []
if StockProduct:
try:
products = StockProduct.query.filter(StockProduct.stock_quantity > 0).all()
except Exception:
products = []
return jsonify({
"materials": [item.to_dict() for item in materials],
"semis": [item.to_dict() for item in semis],
"products": [item.to_dict() for item in products]
}), 200
except Exception as e:
print(f"Error: {e}")
return jsonify({"message": f"查询库存失败: {str(e)}"}), 500
# --- 草稿箱接口 ---
@bp.route('/draft/list', methods=['GET'])
@permission_required('inventory_stocktake')
def get_drafts():
"""
获取当前用户的盘点进度
支持过滤: session_id
"""
session_id = request.args.get('session_id')
query = StocktakeDraft.query
if session_id:
query = query.filter_by(session_id=session_id)
drafts = query.order_by(StocktakeDraft.scan_time.desc()).all()
return jsonify([d.to_dict() for d in drafts]), 200
@bp.route('/draft/add', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def add_draft():
"""
扫码同步 (支持更新数量)
如果 session_id 不存在则创建新的会话
"""
try:
data = request.json
user_id = _normalize_user_id(data.get('user_id', 'admin'))
uuid = data.get('uuid')
quantity = float(data.get('quantity', 1))
session_id = data.get('session_id')
if not uuid:
return jsonify({"message": "UUID不能为空"}), 400
# 如果没有 session_id创建新的
if not session_id:
session_id = f"STK-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid_module.uuid4().hex[:6]}"
# 获取库存信息
item, source_table, stock_id = get_stock_info(uuid)
if not item:
return jsonify({"message": "未找到对应的库存记录"}), 404
stock_qty = float(item.stock_quantity) if item.stock_quantity else 0
# 查找是否已存在
draft = StocktakeDraft.query.filter_by(user_id=user_id, uuid=uuid, session_id=session_id).first()
if draft:
# 如果已存在,更新数量和时间
draft.quantity = quantity
draft.scan_time = datetime.now()
draft.stock_qty = stock_qty
draft.diff_qty = quantity - stock_qty
draft.source_table = source_table
draft.stock_id = stock_id
else:
# 如果不存在,创建新的
draft = StocktakeDraft(
user_id=user_id,
uuid=uuid,
quantity=quantity,
session_id=session_id,
stock_qty=stock_qty,
diff_qty=quantity - stock_qty,
source_table=source_table,
stock_id=stock_id
)
db.session.add(draft)
db.session.commit()
return jsonify({
"message": "Saved",
"session_id": session_id,
"draft_id": draft.id
}), 200
except Exception as e:
print(f"Add Draft Error: {e}")
db.session.rollback()
return jsonify({"message": str(e)}), 500
@bp.route('/draft/clear', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def clear_draft():
"""
清除盘点草稿
支持清除指定 session_id 的记录,或清除所有记录
"""
data = request.json
session_id = data.get('session_id')
try:
query = StocktakeDraft.query
if session_id:
# 清除指定会话
query = query.filter_by(session_id=session_id)
count = query.delete()
db.session.commit()
return jsonify({"message": f"已清除 {count} 条记录", "count": count}), 200
except Exception as e:
db.session.rollback()
return jsonify({"message": str(e)}), 500
@bp.route('/draft/start-new', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def start_new_session():
"""
开始新一轮盘点
清空整张草稿表,返回新的 session_id
"""
try:
# 清空整张草稿表
deleted_count = StocktakeDraft.query.delete()
db.session.commit()
# 生成新的 session_id
new_session_id = f"STK-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid_module.uuid4().hex[:6]}"
return jsonify({
"message": f"已清除 {deleted_count} 条旧记录",
"session_id": new_session_id,
"cleared_count": deleted_count
}), 200
except Exception as e:
db.session.rollback()
return jsonify({"message": str(e)}), 500
# --- 盘点结束与差异报告 ---
@bp.route('/finish', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def finish_stocktake():
"""
结束盘点
直接返回成功,前端会跳转到差异列表
草稿数据保留在表中
"""
return jsonify({
"message": "盘点已结束",
"code": 200
}), 200
@bp.route('/variance-report', methods=['GET'])
@permission_required('inventory_stocktake')
def get_variance_report():
"""
获取盘点差异报告
返回所有有差异的记录diff_qty != 0
"""
session_id = request.args.get('session_id')
try:
query = StocktakeDraft.query
if session_id:
query = query.filter_by(session_id=session_id)
# 只返回有差异的记录
drafts = query.filter(StocktakeDraft.diff_qty != 0).order_by(
StocktakeDraft.scan_time.desc(),
StocktakeDraft.diff_qty.desc()
).all()
# 补充库存详情
result = []
for draft in drafts:
draft_dict = draft.to_dict()
# 获取库存详情
if draft.stock_id and draft.source_table:
stock = get_stock_record(draft.source_table, draft.stock_id)
if stock:
draft_dict['stock_name'] = getattr(stock, 'material_name', None) or \
getattr(stock, 'product_name', None) or ''
draft_dict['stock_spec'] = getattr(stock, 'spec_model', '') or \
getattr(stock, 'standard', '') or ''
draft_dict['stock_location'] = getattr(stock, 'warehouse_location', '') or ''
draft_dict['stock_unit'] = getattr(stock, 'unit', '')
result.append(draft_dict)
return jsonify({
"list": result,
"total": len(result)
}), 200
except Exception as e:
print(f"Error: {e}")
return jsonify({"message": str(e)}), 500
# --- 单条库存调整 (手动平账) ---
@bp.route('/adjust', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def adjust_stock():
"""
单条库存调整接口
接收指定的草稿 ID执行以下操作
1. 根据 diff_qty 调整库存 (stock_quantity 和 available_quantity)
2. 生成流水账记录 (盘盈入库 / 盘亏出库)
3. 标记草稿为 is_processed=True
"""
data = request.json
draft_id = data.get('draft_id')
operator_name = data.get('operator_name', 'System')
remark = data.get('remark', '')
if not draft_id:
return jsonify({"message": "draft_id 不能为空"}), 400
try:
# 1. 获取草稿记录
draft = StocktakeDraft.query.get(draft_id)
if not draft:
return jsonify({"message": "草稿记录不存在"}), 404
# 2. 获取库存记录
if not draft.stock_id or not draft.source_table:
return jsonify({"message": "草稿记录缺少库存关联信息"}), 400
stock = get_stock_record(draft.source_table, draft.stock_id)
if not stock:
return jsonify({"message": "库存记录不存在"}), 404
diff_qty = float(draft.diff_qty)
# 3. 计算调整
if diff_qty > 0:
# 盘盈:增加库存
new_stock_qty = float(stock.stock_quantity or 0) + diff_qty
new_avail_qty = float(stock.available_quantity or 0) + diff_qty
action_type = '盘盈入库'
elif diff_qty < 0:
# 盘亏:减少库存
abs_diff = abs(diff_qty)
current_avail = float(stock.available_quantity or 0)
if current_avail < abs_diff:
return jsonify({
"message": f"可用库存不足,当前可用: {current_avail},需要减少: {abs_diff}"
}), 400
new_stock_qty = float(stock.stock_quantity or 0) - abs_diff
new_avail_qty = current_avail - abs_diff
action_type = '盘亏出库'
else:
return jsonify({"message": "差异为0无需调整"}), 400
# 4. 执行库存调整
stock.stock_quantity = new_stock_qty
stock.available_quantity = new_avail_qty
# 5. 生成流水账记录
# 导入流水账模型
from app.models.outbound import TransOutbound
trans_record = TransOutbound(
outbound_no=f"STKADJ-{datetime.now().strftime('%Y%m%d%H%M%S')}-{draft.id:04d}",
sku=stock.sku,
source_table=draft.source_table,
stock_id=draft.stock_id,
barcode=getattr(stock, 'barcode', ''),
quantity=abs(diff_qty),
unit_price=getattr(stock, 'pre_tax_unit_price', 0) or getattr(stock, 'manual_cost', 0) or 0,
outbound_type=action_type, # 盘盈入库 / 盘亏出库
consumer_name='盘点调整',
operator_name=operator_name,
remark=f"{action_type} - 盘点差异调整,备注: {remark}",
outbound_time=datetime.now()
)
db.session.add(trans_record)
# 6. 删除草稿记录(平账后从工作台移除)
db.session.delete(draft)
db.session.commit()
return jsonify({
"message": f"{action_type}成功",
"action_type": action_type,
"diff_qty": diff_qty,
"new_stock_qty": new_stock_qty,
"new_avail_qty": new_avail_qty,
"draft_id": draft_id
}), 200
except Exception as e:
db.session.rollback()
print(f"Adjust Stock Error: {e}")
return jsonify({"message": str(e)}), 500
@bp.route('/borrowed-quantities', methods=['POST'])
@permission_required('inventory_stocktake')
def get_borrowed_quantities():
"""批量获取借出未还数量"""
data = request.json.get('items', [])
result = {}
for item in data:
source = item.get('source_table')
stock_id = item.get('stock_id')
if source and stock_id is not None:
qty = TransBorrow.get_borrowed_quantity(source, stock_id)
result[f"{source}_{stock_id}"] = qty
return jsonify(result), 200
# --- 打印接口 ---
@bp.route('/print/selection', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def print_selection():
try:
data = request.json
items = data.get('items', [])
if not items: return jsonify({"message": "未选择任何物品"}), 400
printer = NetworkPrintService()
success, msg = printer.print_outbound_selection(items)
return jsonify({"message": "打印指令已发送" if success else msg}), 200 if success else 500
except Exception as e:
return jsonify({"message": str(e)}), 500
@bp.route('/print/stocktake', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def print_stocktake():
try:
data = request.json
printer = NetworkPrintService()
success, msg = printer.print_stocktake_report(data)
return jsonify({"message": "盘点报告已发送" if success else msg}), 200 if success else 500
except Exception as e:
return jsonify({"message": str(e)}), 500