Merge remote-tracking branch 'origin/2.0权限管理' into 2.0权限管理
This commit is contained in:
287
inventory-backend/app/api/v1/scrap.py
Normal file
287
inventory-backend/app/api/v1/scrap.py
Normal file
@ -0,0 +1,287 @@
|
||||
# inventory-backend/app/api/v1/scrap.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, audit_log
|
||||
from app.services.auth_service import AuthService
|
||||
from app.extensions import db
|
||||
from app.models.transaction import TransScrap
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
import traceback
|
||||
import math
|
||||
|
||||
scrap_bp = Blueprint('scrap', __name__, url_prefix='/scrap')
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
if user_role.upper() == 'SUPER_ADMIN':
|
||||
return ['scrap_list:*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
return perms
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 1. 扫码查询库存接口 (关联三个库存表)
|
||||
# GET /api/v1/scrap/scan?barcode=...
|
||||
# --------------------------------------------------------
|
||||
@scrap_bp.route('/scan', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_selection')
|
||||
def scan_barcode():
|
||||
barcode = request.args.get('barcode')
|
||||
if not barcode:
|
||||
return jsonify({'code': 400, 'msg': '请提供条码'}), 400
|
||||
|
||||
try:
|
||||
result = ScrapService.get_stock_by_barcode(barcode)
|
||||
if result:
|
||||
return jsonify({'code': 200, 'msg': '扫描成功', 'data': result})
|
||||
else:
|
||||
return jsonify({'code': 404, 'msg': '未找到对应的库存记录'}), 404
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': f'扫描查询出错: {str(e)}'}), 500
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 2. 提交报废单接口
|
||||
# POST /api/v1/scrap
|
||||
# --------------------------------------------------------
|
||||
@scrap_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@audit_log(
|
||||
module='报废管理',
|
||||
action='报废出库',
|
||||
get_target_name_fn=lambda: request.get_json().get('items')[0].get('sku') if request.get_json() and request.get_json().get('items') else None
|
||||
)
|
||||
def create_scrap():
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return jsonify({'code': 403, 'msg': '未授权'}), 403
|
||||
|
||||
# 超级管理员直接放行
|
||||
if user_role.upper() != 'SUPER_ADMIN':
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
if 'scrap_create:operation' not in perms:
|
||||
return jsonify({'code': 403, 'msg': '权限不足'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
||||
|
||||
current_user_name = get_jwt_identity() or 'Unknown'
|
||||
|
||||
# items 必填
|
||||
if 'items' not in data or not data['items']:
|
||||
return jsonify({'code': 400, 'msg': '报废商品列表不能为空'}), 400
|
||||
|
||||
try:
|
||||
result = ScrapService.process_scrap(data, operator_name=current_user_name)
|
||||
return jsonify({'code': 200, 'msg': '报废成功', 'data': result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
db.session.rollback()
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 3. 报废记录查询接口
|
||||
# GET /api/v1/scrap/records
|
||||
# --------------------------------------------------------
|
||||
@scrap_bp.route('/records', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('scrap_list')
|
||||
def get_scrap_records():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
page_size = request.args.get('pageSize', 50, type=int)
|
||||
sku = request.args.get('sku', '')
|
||||
start_date = request.args.get('start_date', '')
|
||||
end_date = request.args.get('end_date', '')
|
||||
|
||||
try:
|
||||
result = ScrapService.query_records(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
sku=sku,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
return jsonify({'code': 200, 'msg': 'success', 'data': result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Service 层:报废核心逻辑
|
||||
# ============================================================
|
||||
class ScrapService:
|
||||
|
||||
@staticmethod
|
||||
def get_stock_by_barcode(barcode):
|
||||
"""根据条码查找库存"""
|
||||
if not barcode:
|
||||
return None
|
||||
clean_code = barcode.strip()
|
||||
|
||||
def get_price(item, table_type):
|
||||
if table_type == 'stock_product':
|
||||
return float(item.sale_price) if item.sale_price else 0
|
||||
elif table_type == 'stock_buy':
|
||||
return float(item.pre_tax_unit_price) if item.pre_tax_unit_price else 0
|
||||
return 0
|
||||
|
||||
# 1. 查询成品
|
||||
prod = StockProduct.query.filter(
|
||||
db.or_(StockProduct.barcode == clean_code, StockProduct.sku == clean_code)
|
||||
).first()
|
||||
if prod:
|
||||
res = ScrapService._format_stock(prod, 'stock_product')
|
||||
res['price'] = get_price(prod, 'stock_product')
|
||||
return res
|
||||
|
||||
# 2. 查询半成品
|
||||
semi = StockSemi.query.filter(
|
||||
db.or_(StockSemi.barcode == clean_code, StockSemi.sku == clean_code)
|
||||
).first()
|
||||
if semi:
|
||||
res = ScrapService._format_stock(semi, 'stock_semi')
|
||||
res['price'] = 0
|
||||
return res
|
||||
|
||||
# 3. 查询原材料
|
||||
buy = StockBuy.query.filter(
|
||||
db.or_(StockBuy.barcode == clean_code, StockBuy.sku == clean_code)
|
||||
).first()
|
||||
if buy:
|
||||
res = ScrapService._format_stock(buy, 'stock_buy')
|
||||
res['price'] = get_price(buy, 'stock_buy')
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _format_stock(item, table_type):
|
||||
"""格式化库存查询结果"""
|
||||
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': item.sku,
|
||||
'barcode': item.barcode,
|
||||
'name': item.material_base.name if item.material_base else '',
|
||||
'spec': item.material_base.spec_model if item.material_base else '',
|
||||
'category': item.material_base.category if item.material_base else '',
|
||||
'material_type': item.material_base.material_type if item.material_base else '',
|
||||
'warehouse_loc': item.warehouse_loc or '',
|
||||
'stock_quantity': stock_qty,
|
||||
'available_quantity': avail_qty,
|
||||
'source_table': table_type,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def process_scrap(data, operator_name='System'):
|
||||
"""处理报废:扣减库存并记录报废单"""
|
||||
items = data.get('items', [])
|
||||
reason = data.get('reason', '')
|
||||
|
||||
if not reason:
|
||||
raise ValueError('请填写报废原因')
|
||||
|
||||
created_records = []
|
||||
|
||||
for item in items:
|
||||
stock_id = item.get('id')
|
||||
source_table = item.get('source_table')
|
||||
scrap_qty = float(item.get('quantity', 0))
|
||||
|
||||
if not stock_id or not source_table or scrap_qty <= 0:
|
||||
continue
|
||||
|
||||
# 获取库存记录
|
||||
stock_record = None
|
||||
if source_table == 'stock_product':
|
||||
stock_record = StockProduct.query.get(stock_id)
|
||||
elif source_table == 'stock_semi':
|
||||
stock_record = StockSemi.query.get(stock_id)
|
||||
elif source_table == 'stock_buy':
|
||||
stock_record = StockBuy.query.get(stock_id)
|
||||
|
||||
if not stock_record:
|
||||
raise ValueError(f'库存记录不存在: ID={stock_id}')
|
||||
|
||||
# 检查可用数量
|
||||
avail_qty = float(stock_record.available_quantity) if stock_record.available_quantity else 0
|
||||
if avail_qty < scrap_qty:
|
||||
raise ValueError(f"SKU {stock_record.sku} 可用库存不足,当前可用: {avail_qty}")
|
||||
|
||||
# 计算损失金额
|
||||
unit_price = 0.0
|
||||
if source_table == 'stock_product':
|
||||
unit_price = float(stock_record.sale_price) if stock_record.sale_price else 0
|
||||
elif source_table == 'stock_buy':
|
||||
unit_price = float(stock_record.pre_tax_unit_price) if stock_record.pre_tax_unit_price else 0
|
||||
|
||||
total_loss = round(unit_price * scrap_qty, 2)
|
||||
|
||||
# 扣减库存
|
||||
stock_record.stock_quantity = float(stock_record.stock_quantity) - scrap_qty
|
||||
stock_record.available_quantity = float(stock_record.available_quantity) - scrap_qty
|
||||
|
||||
# 创建报废记录
|
||||
scrap_record = TransScrap(
|
||||
sku=stock_record.sku,
|
||||
source_table=source_table,
|
||||
stock_id=stock_id,
|
||||
quantity=scrap_qty,
|
||||
reason=reason,
|
||||
operator_name=operator_name,
|
||||
approval_status='approved',
|
||||
cost_at_scrap=unit_price,
|
||||
total_loss=total_loss
|
||||
)
|
||||
db.session.add(scrap_record)
|
||||
created_records.append(scrap_record)
|
||||
|
||||
db.session.commit()
|
||||
return {'count': len(created_records)}
|
||||
|
||||
@staticmethod
|
||||
def query_records(page=1, page_size=50, sku='', start_date='', end_date=''):
|
||||
"""分页查询报废记录"""
|
||||
query = TransScrap.query
|
||||
|
||||
if sku:
|
||||
query = query.filter(TransScrap.sku.like(f'%{sku}%'))
|
||||
|
||||
if start_date:
|
||||
query = query.filter(TransScrap.operation_time >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(TransScrap.operation_time <= end_date + ' 23:59:59')
|
||||
|
||||
# 按时间倒序
|
||||
query = query.order_by(TransScrap.operation_time.desc())
|
||||
|
||||
total = query.count()
|
||||
records = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return {
|
||||
'list': [r.to_dict() for r in records],
|
||||
'total': total,
|
||||
'page': page,
|
||||
'pageSize': page_size
|
||||
}
|
||||
10
inventory-backend/app/services/print/printer_config.json
Normal file
10
inventory-backend/app/services/print/printer_config.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"label_printer": {
|
||||
"ip": "172.16.0.119",
|
||||
"port": 9100
|
||||
},
|
||||
"network_printer": {
|
||||
"ip": "192.168.9.250",
|
||||
"port": 9100
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user