246 lines
9.5 KiB
Python
246 lines
9.5 KiB
Python
"""仓库路由"""
|
||
from flask import Blueprint, request, jsonify, session, redirect, url_for, render_template
|
||
from services.db import query
|
||
from routes.auth import login_required, role_required
|
||
|
||
warehouse_bp = Blueprint('warehouse', __name__)
|
||
|
||
|
||
@warehouse_bp.route('/warehouse')
|
||
@login_required
|
||
def warehouse_page():
|
||
if not role_required('warehouse'):
|
||
return redirect(url_for('auth.index'))
|
||
return render_template('base.html', page='warehouse')
|
||
|
||
|
||
# ─── API: 仓库管理 ───
|
||
@warehouse_bp.route('/api/warehouse/list')
|
||
@login_required
|
||
def warehouse_list():
|
||
page = int(request.args.get('page', 1))
|
||
size = int(request.args.get('size', 50))
|
||
offset = (page - 1) * size
|
||
|
||
search = request.args.get('search', '')
|
||
category_filter = request.args.get('category', '')
|
||
|
||
where = []
|
||
params = []
|
||
|
||
if search:
|
||
where.append("(warehouse_name LIKE %s OR warehouse_code LIKE %s)")
|
||
params.extend([f"%{search}%", f"%{search}%"])
|
||
if category_filter:
|
||
where.append("category=%s")
|
||
params.append(category_filter)
|
||
|
||
where_clause = " AND ".join(where) if where else "1=1"
|
||
|
||
total = query(f"SELECT COUNT(*) AS cnt FROM warehouses WHERE {where_clause}", params)[0]['cnt']
|
||
rows = query(f"SELECT * FROM warehouses WHERE {where_clause} ORDER BY warehouse_code ASC LIMIT {size} OFFSET {offset}", params)
|
||
|
||
return jsonify({
|
||
"code": 0,
|
||
"data": {
|
||
"total": total,
|
||
"page": page,
|
||
"size": size,
|
||
"rows": rows
|
||
}
|
||
})
|
||
|
||
|
||
@warehouse_bp.route('/api/warehouse/categories')
|
||
@login_required
|
||
def warehouse_categories():
|
||
cats = query("SELECT DISTINCT category FROM warehouses ORDER BY category")
|
||
return jsonify({"code": 0, "data": [c['category'] for c in cats]})
|
||
|
||
|
||
@warehouse_bp.route('/api/warehouse/all')
|
||
@login_required
|
||
def warehouse_all():
|
||
"""获取所有仓库(供其他页面下拉选择使用)"""
|
||
rows = query("SELECT warehouse_code, warehouse_name, category FROM warehouses ORDER BY warehouse_code ASC")
|
||
return jsonify({"code": 0, "data": rows})
|
||
|
||
|
||
@warehouse_bp.route('/api/warehouse/save', methods=['POST'])
|
||
@login_required
|
||
def warehouse_save():
|
||
"""新增/编辑仓库;仓库属性发生变化时刷新依赖属性的共享缓存。"""
|
||
data = request.json
|
||
code = data.get('warehouse_code', '').strip()
|
||
name = data.get('warehouse_name', '').strip()
|
||
category = data.get('category', '').strip()
|
||
remark = data.get('remark', '').strip()
|
||
warehouse_property = data.get('warehouse_property', '').strip()
|
||
disabled = data.get('disabled', '否').strip()
|
||
if not code or not name:
|
||
return jsonify({"code": 1, "msg": "仓库编号和名称不能为空"})
|
||
|
||
exists = query("SELECT id, warehouse_property FROM warehouses WHERE warehouse_code=%s", (code,))
|
||
property_changed = not exists or (exists[0].get('warehouse_property') or '').strip() != warehouse_property
|
||
if exists:
|
||
query("UPDATE warehouses SET warehouse_name=%s, category=%s, remark=%s, warehouse_property=%s, disabled=%s WHERE warehouse_code=%s",
|
||
(name, category, remark, warehouse_property, disabled, code))
|
||
else:
|
||
query("INSERT INTO warehouses (warehouse_code, warehouse_name, category, remark, warehouse_property, disabled) VALUES (%s,%s,%s,%s,%s,%s)",
|
||
(code, name, category, remark, warehouse_property, disabled))
|
||
if property_changed:
|
||
from services.cache import refresh_all_caches
|
||
refresh_all_caches()
|
||
return jsonify({"code": 0, "msg": "保存成功", "cache_refreshed": property_changed})
|
||
|
||
|
||
@warehouse_bp.route('/api/warehouse/delete', methods=['POST'])
|
||
@login_required
|
||
def warehouse_delete():
|
||
"""删除仓库"""
|
||
if session.get("role") not in ("admin", "super_admin"):
|
||
return jsonify({"code": 403, "msg": "仅管理员可操作"})
|
||
data = request.json
|
||
code = data.get('warehouse_code', '').strip()
|
||
if not code:
|
||
return jsonify({"code": 400, "msg": "参数错误"})
|
||
try:
|
||
query("DELETE FROM warehouses WHERE warehouse_code=%s", (code,))
|
||
return jsonify({"code": 0, "msg": "删除成功"})
|
||
except Exception as e:
|
||
return jsonify({"code": 500, "msg": str(e)})
|
||
|
||
|
||
@warehouse_bp.route('/api/warehouse/update_field', methods=['POST'])
|
||
@login_required
|
||
def warehouse_update_field():
|
||
"""更新仓库单字段(用于内联编辑)。仓库属性发生变化时刷新共享缓存。"""
|
||
data = request.json
|
||
code = data.get('warehouse_code', '').strip()
|
||
field = data.get('field', '')
|
||
value = data.get('value', '')
|
||
if not code or not field:
|
||
return jsonify({"code": 400, "msg": "参数错误"})
|
||
allowed = ['warehouse_property', 'disabled']
|
||
if field not in allowed:
|
||
return jsonify({"code": 400, "msg": "不允许的字段"})
|
||
property_changed = False
|
||
if field == 'warehouse_property':
|
||
old_rows = query("SELECT warehouse_property FROM warehouses WHERE warehouse_code=%s", (code,))
|
||
old_value = (old_rows[0].get('warehouse_property') or '').strip() if old_rows else ''
|
||
property_changed = old_value != str(value or '').strip()
|
||
try:
|
||
query(f"UPDATE warehouses SET {field}=%s WHERE warehouse_code=%s", (value, code))
|
||
if property_changed:
|
||
from services.cache import refresh_all_caches
|
||
refresh_all_caches()
|
||
return jsonify({"code": 0, "msg": "更新成功", "cache_refreshed": property_changed})
|
||
except Exception as e:
|
||
return jsonify({"code": 500, "msg": str(e)})
|
||
|
||
|
||
@warehouse_bp.route('/api/warehouse/export_template')
|
||
@login_required
|
||
def warehouse_export_template():
|
||
"""下载仓库导入模板 CSV"""
|
||
import csv, io
|
||
output = io.StringIO()
|
||
writer = csv.writer(output)
|
||
writer.writerow(['仓库编号', '仓库名称', '仓库分类', '备注'])
|
||
writer.writerow(['01', '示例仓库', '内部', '备注信息'])
|
||
csv_data = output.getvalue()
|
||
output.close()
|
||
from flask import Response
|
||
return Response(
|
||
csv_data,
|
||
mimetype='text/csv',
|
||
headers={'Content-Disposition': 'attachment; filename=warehouse_import_template.csv'}
|
||
)
|
||
|
||
|
||
@warehouse_bp.route('/api/warehouse/import', methods=['POST'])
|
||
@login_required
|
||
def warehouse_import():
|
||
"""导入仓库数据(直接上传CSV文件)"""
|
||
import io, csv, re
|
||
|
||
if 'file' not in request.files:
|
||
return jsonify({"code": 400, "msg": "未上传文件"})
|
||
f = request.files['file']
|
||
if f.filename == '':
|
||
return jsonify({"code": 400, "msg": "未选择文件"})
|
||
|
||
raw = f.read()
|
||
|
||
# 编码检测:依次尝试 utf-8-sig → gbk → utf-8
|
||
encodings = ['utf-8-sig', 'gbk', 'utf-8']
|
||
text = None
|
||
for enc in encodings:
|
||
try:
|
||
text = raw.decode(enc)
|
||
break
|
||
except (UnicodeDecodeError, LookupError):
|
||
continue
|
||
if text is None:
|
||
return jsonify({"code": 400, "msg": "无法识别CSV文件编码,请确认文件格式正确"})
|
||
|
||
# 统一换行符
|
||
text = text.replace('\r\n', '\n').replace('\r', '\n')
|
||
lines = [l.strip() for l in text.split('\n') if l.strip()]
|
||
if len(lines) < 2:
|
||
return jsonify({"code": 400, "msg": "CSV文件至少需要表头+1行数据"})
|
||
|
||
# 用 csv.reader 解析(兼容引号内逗号)
|
||
reader = csv.reader(io.StringIO('\n'.join(lines)))
|
||
rows = list(reader)
|
||
if len(rows) < 2:
|
||
return jsonify({"code": 400, "msg": "未解析到有效数据"})
|
||
|
||
# 提取列名
|
||
headers = [h.strip() for h in rows[0]]
|
||
expected = ['仓库编号', '仓库名称', '仓库分类', '备注']
|
||
|
||
# 检查是否包含至少两个预期列
|
||
matched_cols = sum(1 for h in expected if h in headers)
|
||
if matched_cols < 2:
|
||
return jsonify({
|
||
"code": 400,
|
||
"msg": f"CSV列名不匹配\n预期列名:{','.join(expected)}\n实际列名:{','.join(headers)}"
|
||
})
|
||
|
||
# 建立索引映射
|
||
idx_code = headers.index('仓库编号') if '仓库编号' in headers else -1
|
||
idx_name = headers.index('仓库名称') if '仓库名称' in headers else -1
|
||
idx_cat = headers.index('仓库分类') if '仓库分类' in headers else -1
|
||
idx_remark = headers.index('备注') if '备注' in headers else -1
|
||
|
||
success = 0
|
||
errors = []
|
||
for i, row in enumerate(rows[1:], 2):
|
||
if len(row) <= max(idx_code, idx_name):
|
||
errors.append(f"第{i}行:列数不足")
|
||
continue
|
||
code = row[idx_code].strip() if idx_code >= 0 else ''
|
||
name = row[idx_name].strip() if idx_name >= 0 else ''
|
||
category = row[idx_cat].strip() if idx_cat >= 0 else '内部'
|
||
remark = row[idx_remark].strip() if idx_remark >= 0 else ''
|
||
if not code or not name:
|
||
errors.append(f"第{i}行:仓库编号或名称为空")
|
||
continue
|
||
try:
|
||
exists = query("SELECT id FROM warehouses WHERE warehouse_code=%s", (code,))
|
||
if exists:
|
||
query("UPDATE warehouses SET warehouse_name=%s, category=%s, remark=%s WHERE warehouse_code=%s",
|
||
(name, category, remark, code))
|
||
else:
|
||
query("INSERT INTO warehouses (warehouse_code, warehouse_name, category, remark) VALUES (%s,%s,%s,%s)",
|
||
(code, name, category, remark))
|
||
success += 1
|
||
except Exception as e:
|
||
errors.append(f"第{i}行({code}):{str(e)[:50]}")
|
||
|
||
msg = f"导入完成,共处理{success}条"
|
||
if errors:
|
||
msg += f",{len(errors)}条异常(前3条:{';'.join(errors[:3])})"
|
||
return jsonify({"code": 0 if success > 0 else 400, "msg": msg})
|