299 lines
10 KiB
Python
299 lines
10 KiB
Python
# inventory-backend/app/api/v1/stock/adjustment.py
|
|
from flask import Blueprint, request, jsonify
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
|
from app.utils.decorators import permission_required
|
|
from app.extensions import db
|
|
from app.models.stock.adjustment import StockAdjustment
|
|
from app.models.base import MaterialBase
|
|
from app.models.inbound.buy import StockBuy
|
|
from app.models.inbound.semi import StockSemi
|
|
from app.models.inbound.product import StockProduct
|
|
from app.models.inbound.stocktake import StocktakeDraft
|
|
from datetime import datetime
|
|
import random
|
|
import string
|
|
|
|
adjustment_bp = Blueprint('adjustment', __name__, url_prefix='/stock/adjustment')
|
|
|
|
|
|
def generate_order_no():
|
|
"""生成单号 ADJ-YYYYMMDD-XXXX"""
|
|
today = datetime.now().strftime('%Y%m%d')
|
|
suffix = ''.join(random.choices(string.digits, k=4))
|
|
return f'ADJ-{today}-{suffix}'
|
|
|
|
|
|
def get_stock_model(source_table):
|
|
"""根据source_table获取对应的库存模型"""
|
|
if source_table == 'stock_buy':
|
|
return StockBuy
|
|
elif source_table == 'stock_semi':
|
|
return StockSemi
|
|
elif source_table == 'stock_product':
|
|
return StockProduct
|
|
return None
|
|
|
|
|
|
# --------------------------------------------------------
|
|
# 1. 获取调整单列表
|
|
# GET /api/v1/stock/adjustment/list
|
|
# --------------------------------------------------------
|
|
@adjustment_bp.route('/list', methods=['GET'])
|
|
@jwt_required()
|
|
@permission_required('stock_adjustment:list')
|
|
def get_list():
|
|
page = int(request.args.get('page', 1))
|
|
limit = int(request.args.get('limit', 10))
|
|
keyword = request.args.get('keyword', '')
|
|
adjust_type = request.args.get('adjust_type', '')
|
|
status = request.args.get('status', '')
|
|
|
|
query = StockAdjustment.query
|
|
|
|
if keyword:
|
|
query = query.filter(
|
|
db.or_(
|
|
StockAdjustment.order_no.ilike(f'%{keyword}%'),
|
|
StockAdjustment.sku.ilike(f'%{keyword}%'),
|
|
StockAdjustment.material_name.ilike(f'%{keyword}%')
|
|
)
|
|
)
|
|
|
|
if adjust_type:
|
|
query = query.filter(StockAdjustment.adjust_type == adjust_type)
|
|
|
|
if status:
|
|
query = query.filter(StockAdjustment.status == status)
|
|
|
|
# 按创建时间降序
|
|
query = query.order_by(StockAdjustment.create_time.desc())
|
|
|
|
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
|
|
|
return jsonify({
|
|
'code': 200,
|
|
'data': {
|
|
'items': [item.to_dict() for item in pagination.items],
|
|
'total': pagination.total,
|
|
'page': page,
|
|
'limit': limit
|
|
}
|
|
})
|
|
|
|
|
|
# --------------------------------------------------------
|
|
# 2. 创建调整单
|
|
# POST /api/v1/stock/adjustment/create
|
|
# --------------------------------------------------------
|
|
@adjustment_bp.route('/create', methods=['POST'])
|
|
@jwt_required()
|
|
@permission_required('stock_adjustment:operation')
|
|
def create():
|
|
data = request.get_json()
|
|
if not data:
|
|
return jsonify({'code': 400, 'msg': '请求参数不能为空'}), 400
|
|
|
|
# 必填字段验证
|
|
required_fields = ['source_table', 'stock_id', 'adjust_type', 'adjust_quantity', 'reason']
|
|
for field in required_fields:
|
|
if field not in data or not data.get(field):
|
|
return jsonify({'code': 400, 'msg': f'{field} 为必填项'}), 400
|
|
|
|
source_table = data['source_table']
|
|
stock_id = int(data['stock_id'])
|
|
adjust_type = data['adjust_type'] # 'profit' or 'loss'
|
|
adjust_quantity = float(data['adjust_quantity'])
|
|
reason = data['reason']
|
|
operator = get_jwt_identity() or 'system'
|
|
|
|
# 获取库存记录
|
|
StockModel = get_stock_model(source_table)
|
|
if not StockModel:
|
|
return jsonify({'code': 400, 'msg': '无效的库存类型'}), 400
|
|
|
|
stock = StockModel.query.get(stock_id)
|
|
if not stock:
|
|
return jsonify({'code': 404, 'msg': '库存记录不存在'}), 404
|
|
|
|
# 获取物料信息
|
|
base_id = getattr(stock, 'base_id', None)
|
|
material = MaterialBase.query.get(base_id) if base_id else None
|
|
|
|
# 计算库存变动
|
|
if adjust_type == 'profit':
|
|
# 盘盈:增加库存
|
|
new_stock_qty = float(stock.stock_quantity or 0) + adjust_quantity
|
|
new_avail_qty = float(stock.available_quantity or 0) + adjust_quantity
|
|
elif adjust_type == 'loss':
|
|
# 盘亏:减少库存
|
|
new_stock_qty = float(stock.stock_quantity or 0) - adjust_quantity
|
|
new_avail_qty = float(stock.available_quantity or 0) - adjust_quantity
|
|
if new_stock_qty < 0 or new_avail_qty < 0:
|
|
return jsonify({'code': 400, 'msg': '库存不足,无法盘亏'}), 400
|
|
else:
|
|
return jsonify({'code': 400, 'msg': '无效的调整类型'}), 400
|
|
|
|
try:
|
|
# 创建调整单
|
|
adjustment = StockAdjustment(
|
|
order_no=generate_order_no(),
|
|
base_id=base_id,
|
|
stock_id=stock_id,
|
|
source_table=source_table,
|
|
sku=getattr(stock, 'sku', None) or getattr(stock, 'SKU', None),
|
|
material_name=material.name if material else getattr(stock, 'sku', '未知'),
|
|
spec_model=getattr(material, 'spec_model', None) if material else None,
|
|
warehouse_location=getattr(stock, 'warehouse_location', None),
|
|
adjust_type=adjust_type,
|
|
adjust_quantity=adjust_quantity,
|
|
reason=reason,
|
|
status='completed',
|
|
operator=operator
|
|
)
|
|
db.session.add(adjustment)
|
|
|
|
# 更新库存
|
|
stock.stock_quantity = new_stock_qty
|
|
stock.available_quantity = new_avail_qty
|
|
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'code': 200,
|
|
'msg': '调整成功',
|
|
'data': adjustment.to_dict()
|
|
})
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return jsonify({'code': 500, 'msg': f'调整失败: {str(e)}'}), 500
|
|
|
|
|
|
# --------------------------------------------------------
|
|
# 3. 获取库存列表(用于选择物料)
|
|
# GET /api/v1/stock/adjustment/stocks
|
|
# --------------------------------------------------------
|
|
@adjustment_bp.route('/stocks', methods=['GET'])
|
|
@jwt_required()
|
|
@permission_required('stock_adjustment:list')
|
|
def get_stocks():
|
|
"""获取可用于调整的库存列表"""
|
|
source_table = request.args.get('source_table', 'stock_buy')
|
|
keyword = request.args.get('keyword', '')
|
|
page = int(request.args.get('page', 1))
|
|
limit = int(request.args.get('limit', 20))
|
|
|
|
StockModel = get_stock_model(source_table)
|
|
if not StockModel:
|
|
return jsonify({'code': 400, 'msg': '无效的库存类型'}), 400
|
|
|
|
query = StockModel.query.filter(StockModel.stock_quantity > 0)
|
|
|
|
if keyword:
|
|
query = query.filter(
|
|
db.or_(
|
|
StockModel.sku.ilike(f'%{keyword}%'),
|
|
StockModel.barcode.ilike(f'%{keyword}%')
|
|
)
|
|
)
|
|
|
|
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
|
|
|
items = []
|
|
for stock in pagination.items:
|
|
base_id = getattr(stock, 'base_id', None)
|
|
material = MaterialBase.query.get(base_id) if base_id else None
|
|
items.append({
|
|
'stock_id': stock.id,
|
|
'source_table': source_table,
|
|
'sku': getattr(stock, 'sku', None) or getattr(stock, 'SKU', None),
|
|
'material_name': material.name if material else getattr(stock, 'sku', '未知'),
|
|
'spec_model': getattr(material, 'spec_model', None) if material else None,
|
|
'stock_quantity': float(stock.stock_quantity or 0),
|
|
'available_quantity': float(stock.available_quantity or 0),
|
|
'warehouse_location': getattr(stock, 'warehouse_location', None),
|
|
})
|
|
|
|
return jsonify({
|
|
'code': 200,
|
|
'data': {
|
|
'items': items,
|
|
'total': pagination.total,
|
|
'page': page,
|
|
'limit': limit
|
|
}
|
|
})
|
|
|
|
|
|
# --------------------------------------------------------
|
|
# 5. 一键引入盘点差异
|
|
# POST /api/v1/stock/adjustment/import-from-stocktake
|
|
# --------------------------------------------------------
|
|
@adjustment_bp.route('/import-from-stocktake', methods=['POST'])
|
|
@jwt_required()
|
|
@permission_required('stock_adjustment:operation')
|
|
def import_from_stocktake():
|
|
"""从盘点差异记录导入为盘盈盘亏单"""
|
|
identity = get_jwt_identity()
|
|
operator = identity.get('username', 'system') if isinstance(identity, dict) else str(identity)
|
|
|
|
try:
|
|
# 查询所有有差异的盘点记录
|
|
drafts = StocktakeDraft.query.filter(StocktakeDraft.diff_qty != 0).all()
|
|
|
|
if not drafts:
|
|
return jsonify({'code': 200, 'msg': '暂无盘点差异记录', 'data': {'count': 0}})
|
|
|
|
count = 0
|
|
for draft in drafts:
|
|
# 判断盘盈/盘亏
|
|
diff = float(draft.diff_qty or 0)
|
|
if diff == 0:
|
|
continue
|
|
|
|
adjust_type = 'profit' if diff > 0 else 'loss'
|
|
adjust_quantity = abs(diff)
|
|
|
|
# 获取物料基础信息
|
|
base_id = None
|
|
sku = ''
|
|
warehouse_location = ''
|
|
|
|
# 根据source_table获取对应的库存记录
|
|
stock_model = get_stock_model(draft.source_table)
|
|
if stock_model and draft.stock_id:
|
|
stock = stock_model.query.get(draft.stock_id)
|
|
if stock:
|
|
base_id = getattr(stock, 'base_id', None)
|
|
sku = getattr(stock, 'sku', None) or getattr(stock, 'SKU', '')
|
|
warehouse_location = getattr(stock, 'warehouse_location', '')
|
|
|
|
# 生成调整单号
|
|
order_no = generate_order_no()
|
|
|
|
# 使用备注或默认原因
|
|
reason = draft.remark if draft.remark else '盘点差异自动生成'
|
|
|
|
# 创建调整单
|
|
adjustment = StockAdjustment(
|
|
order_no=order_no,
|
|
base_id=base_id,
|
|
stock_id=draft.stock_id,
|
|
source_table=draft.source_table,
|
|
sku=sku,
|
|
warehouse_location=warehouse_location,
|
|
adjust_type=adjust_type,
|
|
adjust_quantity=adjust_quantity,
|
|
reason=reason,
|
|
status='pending',
|
|
operator=operator
|
|
)
|
|
db.session.add(adjustment)
|
|
count += 1
|
|
|
|
db.session.commit()
|
|
return jsonify({'code': 200, 'msg': '导入成功', 'data': {'count': count}})
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
return jsonify({'code': 500, 'msg': f'导入失败: {str(e)}'}), 500
|