dy-dashboard/routes/purchase.py.backup-0630
2026-10-28 11:14:17 +08:00

1214 lines
50 KiB
Plaintext
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 datetime import datetime
from flask import Blueprint, request, jsonify, session, redirect, url_for, render_template
from services.db import query, add_operation_log
from services.sales_calc import compute_product_sales_cached
from services.inventory_calc import (compute_product_inventory_cached,
compute_product_inventory_15,
compute_product_in_transit,
_determine_trend)
from services.forecast import compute_daily_forecast, sales_predict_config_cached
from routes.auth import login_required, role_required
purchase_bp = Blueprint('purchase', __name__)
@purchase_bp.route('/purchase')
@login_required
def purchase_page():
return render_template('base.html', page='purchase')
@purchase_bp.route('/orders')
@login_required
def orders_page():
return render_template('base.html', page='orders')
@purchase_bp.route('/orders/domestic')
@login_required
def orders_domestic_page():
return render_template('base.html', page='orders_domestic')
@purchase_bp.route('/orders/overseas')
@login_required
def orders_overseas_page():
return render_template('base.html', page='orders_overseas')
@purchase_bp.route('/orders/packaging')
@login_required
def orders_packaging_page():
return render_template('base.html', page='orders_packaging')
@purchase_bp.route('/purchase/ready')
@login_required
def purchase_ready_page():
return render_template('base.html', page='purchase_ready')
@purchase_bp.route('/purchase/confirmed')
@login_required
def purchase_confirmed_page():
return render_template('base.html', page='purchase_confirmed')
# ─── API: 采购订单 ───
@purchase_bp.route('/api/purchase/pending_list')
@login_required
def purchase_pending_list():
"""获取待下单/已下单商品列表"""
status_filter = request.args.get('status', 'pending')
rows = query("""
SELECT id, product_code, product_name, product_type, product_grade,
supplier_name, lead_time, logistics_time, total_support_days,
procurement_follower, procurement_planner, platform, operator, created_by,
audit_price, formal_order_qty, suggested_qty, actual_order_qty, bom_items,
status, created_at, updated_at, ordered_at, remark
FROM pending_orders
WHERE status = %s
ORDER BY ordered_at DESC, created_at DESC
""", (status_filter,))
# 格式化日期时间字段为字符串
for r in rows:
r['suggested_qty'] = int(r.get('suggested_qty') or 0)
r['actual_order_qty'] = int(r.get('actual_order_qty') or 0)
for key in ['created_at', 'updated_at', 'ordered_at']:
v = r.get(key)
if v and hasattr(v, 'strftime'):
r[key] = v.strftime('%Y-%m-%d %H:%M:%S')
elif v and not isinstance(v, str):
r[key] = str(v)
return jsonify({"code": 0, "data": rows})
@purchase_bp.route('/api/purchase/update_formal_qty', methods=['POST'])
@login_required
def purchase_update_formal_qty():
"""仅更新正式下单数量,不创建待下单记录"""
data = request.json
pc = data.get('product_code', '')
qty = int(data.get('formal_order_qty', 0))
if not pc:
return jsonify({"code": 400, "msg": "缺少货品编号"})
# 保存到待下单表(标记为草稿,不显示在待下单列表中)
existing = query("SELECT id FROM pending_orders WHERE product_code=%s", (pc,))
if existing:
query("UPDATE pending_orders SET formal_order_qty=%s, updated_at=NOW() WHERE product_code=%s",
(qty, pc))
else:
# 查询产品信息补全必填字段
prod = query("SELECT product_name, product_type FROM products WHERE product_code=%s", (pc,))
pname = prod[0]['product_name'] if prod else ''
ptype = prod[0]['product_type'] if prod else ''
query("""INSERT INTO pending_orders
(product_code, product_name, product_type, formal_order_qty, status, created_by)
VALUES (%s, %s, %s, %s, 'draft', %s)""",
(pc, pname, ptype, qty, session.get('real_name', '') or session.get('username', '')))
add_operation_log('修改下单数量', f'{pc} → {qty}')
return jsonify({"code": 0, "msg": "已更新"})
@purchase_bp.route('/api/purchase/check_pending')
@login_required
def purchase_check_pending():
"""检查单个商品是否已在待下单列表"""
code = request.args.get('code', '')
if not code:
return jsonify({"code": 400, "exists": False})
existing = query("SELECT id FROM pending_orders WHERE product_code=%s AND status='pending'", (code,))
return jsonify({"code": 0, "exists": bool(existing)})
@purchase_bp.route('/api/purchase/add_pending', methods=['POST'])
@login_required
def purchase_add_pending():
"""将商品加入待下单列表"""
data = request.json
pc = data.get('product_code', '')
if not pc:
return jsonify({"code": 400, "msg": "缺少货品编号"})
# 检查是否已存在待下单记录
# 如果是更新操作(携带更新字段),跳过重复检查
keys = set(data.keys())
update_fields = {'formal_order_qty', 'audit_price', 'actual_order_qty', 'suggested_qty', 'remark', 'bom_items'}
is_update = bool(keys & update_fields)
if not is_update:
existing = query("SELECT id FROM pending_orders WHERE product_code=%s AND status='pending'", (pc,))
if existing:
return jsonify({"code": 400, "msg": f"[{pc}] 已在待下单列表中,请勿重复添加"})
# 查询该产品的BOM物料清单
# 如果请求带了bom_items,优先使用
req_bom_items = data.get('bom_items', '')
bom_json = None
if req_bom_items:
import json
try:
bom_json = json.loads(req_bom_items) if isinstance(req_bom_items, str) else req_bom_items
# 确保每个物料有material_code和actual_quantity
bom_json = [{
'material_code': b.get('material_code', ''),
'material_name': b.get('material_name', ''),
'quantity': float(b.get('quantity', 0)),
'actual_quantity': int(b.get('actual_quantity', 0)),
'remark': b.get('remark', ''),
'change_manuscript': b.get('change_manuscript', '')
} for b in bom_json if b.get('material_code')]
bom_json = json.dumps(bom_json, ensure_ascii=False) if bom_json else None
except:
bom_json = None
if not bom_json:
bom_items = query("""
SELECT material_code, material_name, quantity
FROM bom WHERE product_code=%s ORDER BY material_code
""", (pc,))
bom_json = None
if bom_items:
import json
bom_items_res = [{'material_code': r['material_code'], 'material_name': r['material_name'], 'quantity': float(r['quantity'])} for r in bom_items]
if bom_items_res:
bom_json = json.dumps(bom_items_res, ensure_ascii=False)
# 从请求数据取值,空字段从 products 表补全
pn = data.get('product_name', '')
pt = data.get('product_type', '')
pg = data.get('product_grade', '')
sn = data.get('supplier_name', '')
lt = data.get('lead_time', 0)
lo = data.get('logistics_time', 0)
ts = data.get('total_support_days', 0)
pf = data.get('procurement_follower', '')
pp = data.get('procurement_planner', '')
pl = data.get('platform', '')
op = data.get('operator', '')
if not pn or not pt:
prod = query("SELECT product_name, product_type, supplier_name_match, lead_time, logistics_time, procurement_follower, procurement_planner, platform, operator FROM products WHERE product_code=%s", (pc,))
if prod:
p = prod[0]
if not pn: pn = p['product_name'] or ''
if not pt: pt = p['product_type'] or ''
if not sn: sn = p['supplier_name_match'] or ''
if not lt: lt = int(p['lead_time'] or 0)
if not lo: lo = int(p['logistics_time'] or 0)
if not pf: pf = p['procurement_follower'] or ''
if not pp: pp = p['procurement_planner'] or ''
if not pl: pl = p['platform'] or ''
if not op: op = p['operator'] or ''
formal_qty = int(data.get('formal_order_qty', 0))
suggested_qty = int(data.get('suggested_qty', 0))
actual_order_qty = int(data.get('actual_order_qty', 0))
created_by = session.get('real_name', '') or session.get('username', '')
remark_val = data.get('remark', '')
query("""
INSERT INTO pending_orders
(product_code, product_name, product_type, product_grade, supplier_name,
lead_time, logistics_time, total_support_days,
procurement_follower, procurement_planner, platform, operator, created_by,
audit_price, formal_order_qty, suggested_qty, actual_order_qty, bom_items, remark)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
product_name=VALUES(product_name),
product_type=VALUES(product_type),
product_grade=VALUES(product_grade),
supplier_name=VALUES(supplier_name),
lead_time=VALUES(lead_time),
logistics_time=VALUES(logistics_time),
total_support_days=VALUES(total_support_days),
procurement_follower=VALUES(procurement_follower),
procurement_planner=VALUES(procurement_planner),
platform=VALUES(platform),
operator=VALUES(operator),
created_by=VALUES(created_by),
audit_price=VALUES(audit_price),
formal_order_qty=VALUES(formal_order_qty),
suggested_qty=VALUES(suggested_qty),
actual_order_qty=VALUES(actual_order_qty),
bom_items=VALUES(bom_items),
remark=VALUES(remark),
status='pending',
updated_at=NOW()
""", (
pc,
pn, pt, pg, sn,
int(lt), int(lo), float(ts),
pf, pp, pl, op,
created_by,
float(data.get('audit_price', 0)),
formal_qty,
suggested_qty,
actual_order_qty,
bom_json,
remark_val
))
return jsonify({"code": 0, "msg": "已加入待下单列表"})
@purchase_bp.route('/api/purchase/update_pending', methods=['POST'])
@login_required
def purchase_update_pending():
"""更新待下单记录的备注等字段"""
data = request.json
product_code = data.get('product_code', '')
if not product_code:
return jsonify({"code": 400, "msg": "参数错误"})
# 支持更新的字段
remark = data.get('remark')
if remark is not None:
query("UPDATE pending_orders SET remark=%s WHERE product_code=%s", (remark, product_code))
add_operation_log('待下单编辑', f'更新 {product_code} 备注')
return jsonify({"code": 0, "msg": "更新成功"})
@purchase_bp.route('/api/purchase/remove_pending', methods=['POST'])
@login_required
def purchase_remove_pending():
"""从待下单列表中移除"""
data = request.json
product_codes = data.get('product_codes', [])
if not product_codes:
return jsonify({"code": 400, "msg": "缺少参数"})
placeholders = ','.join(['%s'] * len(product_codes))
query(f"DELETE FROM pending_orders WHERE product_code IN ({placeholders})", product_codes)
for pc in product_codes:
add_operation_log('移出待下单', f'从待下单列表移除 {pc}')
return jsonify({"code": 0, "msg": "已移除"})
@purchase_bp.route('/api/purchase/confirm_order', methods=['POST'])
@login_required
def purchase_confirm_order():
"""确认下单:备份到下单管理,按采购计划人员分流到国内/海外订单"""
data = request.json
product_codes = data.get('product_codes', [])
if not product_codes:
return jsonify({"code": 400, "msg": "缺少参数"})
now = datetime.now()
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
today = now.strftime('%Y-%m-%d')
for pc in product_codes:
# 1. 查询待下单记录
rows = query("SELECT * FROM pending_orders WHERE product_code=%s AND status='pending'", (pc,))
if not rows:
continue
row = rows[0]
# 2. 判断分流目标(采购计划=张莹纯→海外,其他→国内)
planner = (row.get('procurement_planner') or '').strip()
is_overseas = ('张莹纯' in planner)
# 3. 计算金额
qty = row.get('formal_order_qty') or 0
price = float(row.get('audit_price') or 0)
amount = qty * price
if is_overseas:
# 写入海外订单表(含全部字段)
query("""INSERT INTO overseas_orders
(日期,计划下单时间,产品编码,product_type,product_grade,lead_time,logistics_time,total_support_days,procurement_planner,platform,operator,单价,audit_price,suggested_qty,actual_order_qty,数量,金额,供应商,货品名称,采购员,product_name,下单备注)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""", (
today, now_str,
row.get('product_code'),
row.get('product_type', ''),
row.get('product_grade', ''),
row.get('lead_time', 0),
row.get('logistics_time', 0),
row.get('total_support_days', 0),
row.get('procurement_planner', ''),
row.get('platform', ''),
row.get('operator', ''),
price,
price,
row.get('suggested_qty', 0),
row.get('actual_order_qty', 0),
qty,
amount,
row.get('supplier_name'),
row.get('product_name'),
row.get('procurement_follower'),
row.get('product_name', ''),
row.get('remark', '')
))
else:
# 写入国内订单表(含全部字段)
query("""INSERT INTO domestic_orders
(日期,计划下单时间,编码,名称,product_type,product_grade,lead_time,logistics_time,total_support_days,procurement_planner,platform,operator,单价,audit_price,suggested_qty,actual_order_qty,数量,金额,供货商,采购员,下单备注)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""", (
today, now_str,
row.get('product_code'),
row.get('product_name'),
row.get('product_type', ''),
row.get('product_grade', ''),
row.get('lead_time', 0),
row.get('logistics_time', 0),
row.get('total_support_days', 0),
row.get('procurement_planner', ''),
row.get('platform', ''),
row.get('operator', ''),
price,
price,
row.get('suggested_qty', 0),
row.get('actual_order_qty', 0),
qty,
amount,
row.get('supplier_name'),
row.get('procurement_follower'),
row.get('remark', '')
))
# 4. 根据物料清单生成包材下单(写入包材订单表)
bom_raw = row.get('bom_items')
if bom_raw:
import json
try:
bom_list = json.loads(bom_raw) if isinstance(bom_raw, str) else bom_raw
except:
bom_list = []
for bm in bom_list:
mat_code = bm.get('material_code', '')
mat_name = bm.get('material_name', '')
# 优先使用保存的实际下单数量,没有则用量×正式数量
actual_qty = bm.get('actual_quantity', 0)
if actual_qty:
order_qty = int(actual_qty)
else:
mat_qty = float(bm.get('quantity', 0))
order_qty = int(mat_qty * qty)
if mat_code and order_qty > 0:
mat_amount = round(order_qty * price, 2) if price else 0
query("""INSERT INTO packaging_orders
(日期,下单时间,编码,名称,单价,数量,合计,供应商,下单备注,进度,采购下单人)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""", (
today, now_str, mat_code, mat_name,
price, order_qty, mat_amount,
row.get('supplier_name'),
bm.get('remark', ''),
'',
row.get('procurement_follower')
))
# 5. 标记为已下单(存档到下单备份管理)
query("UPDATE pending_orders SET ordered_at=%s, status='ordered' WHERE product_code=%s AND status='pending'",
(now_str, pc))
return jsonify({"code": 0, "msg": f"已确认 {len(product_codes)} 个商品"})
@purchase_bp.route('/api/purchase/create_material_order', methods=['POST'])
@login_required
def purchase_create_material_order():
"""为物料(BOM配套)生成采购订单"""
data = request.json
pc = data.get('product_code', '')
material_code = data.get('material_code', '')
material_name = data.get('material_name', '')
order_qty = int(data.get('order_qty', 0))
if not material_code or order_qty <= 0:
return jsonify({"code": 400, "msg": "参数错误"})
now = datetime.now()
date_prefix = now.strftime('%Y%m%d')
prefix = f"DYCG{date_prefix}"
last = query("SELECT order_no FROM purchase_orders WHERE order_no LIKE %s ORDER BY id DESC LIMIT 1",
(prefix + '%',))
if last:
last_seq = int(last[0]['order_no'][-4:])
seq = last_seq + 1
else:
seq = 1
order_no = f"{prefix}{seq:04d}"
# 查找物料成本价(作为单价)
prod = query("SELECT cost_price FROM products WHERE product_code=%s", (material_code,))
cost_price = float(prod[0]['cost_price'] or 0) if prod else 0
amount = round(order_qty * cost_price, 2)
try:
order_id = query(
"INSERT INTO purchase_orders (order_no, supplier_name, total_qty, total_amount, created_by) "
"VALUES (%s, %s, %s, %s, %s)",
(order_no, material_name, order_qty, amount, session.get('real_name', ''))
)
query(
"INSERT INTO purchase_order_items (order_id, product_code, product_name, specification, supplier_name, "
"suggested_qty, order_qty, unit_price, amount, lead_time) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
(order_id, material_code, material_name, '', material_name,
0, order_qty, cost_price, amount, 0)
)
query("""INSERT INTO dashboard_logs (user_id, action, detail)
VALUES (%s, %s, %s)""",
(session.get('user_id'), f"生成物料采购单 {material_code}", f"数量:{order_qty} 金额:{amount}"))
return jsonify({"code": 0, "order_no": order_no, "msg": "物料采购单已生成"})
except Exception as e:
return jsonify({"code": 500, "msg": str(e)})
@purchase_bp.route('/api/purchase/batch_add_pending', methods=['POST'])
@login_required
def purchase_batch_add_pending():
"""批量将商品加入待下单列表(从产品表同步数据)"""
data = request.json
product_codes = data.get('product_codes', [])
if not product_codes:
return jsonify({"code": 400, "msg": "缺少参数"})
# 查这些产品的基础信息
placeholders = ','.join(['%s'] * len(product_codes))
products = query(f"""
SELECT p.product_code, p.product_name, p.product_type,
p.lead_time, p.logistics_time, p.supplier_name_match AS supplier_name,
p.procurement_follower, p.procurement_planner, p.platform, p.operator
FROM products p WHERE p.product_code IN ({placeholders})
""", product_codes)
prod_map = {r['product_code']: r for r in products}
added = 0
skipped = 0
for pc in product_codes:
# 检查是否已存在
existing = query("SELECT id FROM pending_orders WHERE product_code=%s AND status='pending'", (pc,))
if existing:
skipped += 1
continue
p = prod_map.get(pc, {})
# 查询BOM物料清单
bom_rows = query("SELECT material_code, material_name, quantity FROM bom WHERE product_code=%s", (pc,))
import json
bom_data = json.dumps([{
'material_code': r['material_code'],
'material_name': r['material_name'],
'quantity': float(r['quantity'])
} for r in bom_rows], ensure_ascii=False) if bom_rows else None
query("""
INSERT INTO pending_orders
(product_code, product_name, product_type, product_grade, supplier_name,
lead_time, logistics_time, total_support_days,
procurement_follower, procurement_planner, platform, operator,
audit_price, formal_order_qty, bom_items)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
pc, p.get('product_name', ''), p.get('product_type', ''), '',
p.get('supplier_name', ''), int(p.get('lead_time', 0) or 0),
int(p.get('logistics_time', 0) or 0), 0,
p.get('procurement_follower', '') or '', p.get('procurement_planner', '') or '',
p.get('platform', '') or '', p.get('operator', '') or '', 0, 0, bom_data
))
added += 1
if added > 0:
add_operation_log('批量加入待下单', f'批量添加 {added} 个商品到待下单列表(跳过 {skipped} 个)')
return jsonify({"code": 0, "data": {"added": added, "skipped": skipped}})
@purchase_bp.route('/api/purchase/create_order', methods=['POST'])
@login_required
def purchase_create_order():
"""生成采购订单"""
data = request.json
supplier_name = data.get('supplier_name', '')
items = data.get('items', [])
if not supplier_name or not items:
return jsonify({"code": 400, "msg": "参数错误"})
now = datetime.now()
date_prefix = now.strftime('%Y%m%d')
prefix = f"DYCG{date_prefix}"
last = query("SELECT order_no FROM purchase_orders WHERE order_no LIKE %s ORDER BY id DESC LIMIT 1",
(prefix + '%',))
if last:
last_seq = int(last[0]['order_no'][-4:])
seq = last_seq + 1
else:
seq = 1
order_no = f"{prefix}{seq:04d}"
total_qty = sum(item.get('order_qty', 0) for item in items)
total_amount = sum(item.get('order_qty', 0) * item.get('unit_price', 0) for item in items)
try:
order_id = query(
"INSERT INTO purchase_orders (order_no, supplier_name, total_qty, total_amount, created_by) "
"VALUES (%s, %s, %s, %s, %s)",
(order_no, supplier_name, total_qty, round(total_amount, 2), session.get('real_name', ''))
)
for item in items:
query(
"INSERT INTO purchase_order_items (order_id, product_code, product_name, specification, supplier_name, "
"suggested_qty, order_qty, unit_price, amount, lead_time) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
(order_id, item.get('product_code', ''), item.get('product_name', ''),
item.get('specification', ''), supplier_name,
item.get('suggested_qty', 0), item.get('order_qty', 0),
item.get('unit_price', 0), round(item.get('order_qty', 0) * item.get('unit_price', 0), 2),
item.get('lead_time', 0))
)
query("INSERT INTO dashboard_logs (user_id, action, detail) VALUES (%s, '创建采购订单', %s)",
(session['user_id'], f"供应商:{supplier_name} 单号:{order_no} 金额:¥{round(total_amount,2)}"))
return jsonify({"code": 0, "msg": "采购订单创建成功", "data": {"order_no": order_no, "order_id": order_id}})
except Exception as e:
return jsonify({"code": 500, "msg": str(e)})
# ─── API: 采购订单列表 ───
@purchase_bp.route('/api/purchase/orders/list')
@login_required
def purchase_orders_list():
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 20))
offset = (page - 1) * size
order_type = request.args.get('type', '') # ''=全部, domestic=国内, overseas=海外, packaging=包材
where_extra = ''
params = []
if order_type == 'domestic':
where_extra = "AND (p.product_type IN ('成品','赠品','小样','物料','其他') OR p.product_type IS NULL)"
elif order_type == 'overseas':
where_extra = "AND p.product_type = '成品' AND (i.product_code LIKE '%%-TMGJ' OR i.product_code LIKE '%%-JDGJ' OR i.product_code LIKE '%%-JDBZ' OR i.product_code LIKE '%%-JDJWT')"
elif order_type == 'packaging':
where_extra = "AND p.product_type = '包材'"
total_count = query(f"""
SELECT COUNT(*) AS cnt FROM purchase_order_items i
JOIN purchase_orders o ON i.order_id = o.id
LEFT JOIN products p ON i.product_code = p.product_code
WHERE 1=1 {where_extra}
""", params)
total = total_count[0]['cnt'] if total_count else 0
items = query(f"""
SELECT o.id AS order_id, i.id AS item_id, o.status,
o.order_no, o.supplier_name, o.created_at AS order_time,
i.product_code, i.product_name, i.specification,
i.unit_price, i.order_qty, i.amount, i.lead_time,
COALESCE(p.product_type, '') AS product_type
FROM purchase_order_items i
JOIN purchase_orders o ON i.order_id = o.id
LEFT JOIN products p ON i.product_code = p.product_code
WHERE 1=1 {where_extra}
ORDER BY o.created_at DESC, i.id ASC
LIMIT %s OFFSET %s
""", params + [size, offset])
for row in items:
if row.get('order_time'):
try:
row['order_time'] = row['order_time'].strftime('%Y-%m-%d %H:%M') if hasattr(row['order_time'], 'strftime') else str(row['order_time'])[:16]
except:
pass
return jsonify({"code": 0, "data": {"total": total, "rows": items}})
@purchase_bp.route('/api/purchase/orders/by_product')
@login_required
def purchase_orders_by_product():
"""根据货品编码查询采购订单在途数据"""
code = request.args.get('code', '')
if not code:
return jsonify({"code": 400, "msg": "参数错误"})
items = query("""
SELECT o.id AS order_id, o.order_no, o.status, o.created_at AS order_time,
i.product_code, i.product_name, i.specification,
i.unit_price, i.order_qty, i.amount, i.lead_time, i.suggested_qty
FROM purchase_order_items i
JOIN purchase_orders o ON i.order_id = o.id
WHERE i.product_code = %s
ORDER BY o.created_at DESC
""", (code,))
for row in items:
if row.get('order_time'):
row['order_time'] = row['order_time'].strftime('%Y-%m-%d') if hasattr(row['order_time'], 'strftime') else str(row['order_time'])[:10]
# 汇总
total_in_transit = sum(r['order_qty'] or 0 for r in items)
total_amount = sum(float(r['amount'] or 0) for r in items)
pending_count = sum(1 for r in items if r['status'] == 'pending')
return jsonify({
"code": 0,
"data": {
"items": items,
"summary": {
"total_orders": len(items),
"total_qty": total_in_transit,
"total_amount": round(total_amount, 2),
"pending_count": pending_count
}
}
})
@purchase_bp.route('/api/purchase/orders/detail')
@login_required
def purchase_orders_detail():
order_id = request.args.get('id')
if not order_id:
return jsonify({"code": 400, "msg": "参数错误"})
order = query("SELECT * FROM purchase_orders WHERE id=%s", (order_id,))
if not order:
return jsonify({"code": 404, "msg": "订单不存在"})
items = query("SELECT * FROM purchase_order_items WHERE order_id=%s", (order_id,))
return jsonify({"code": 0, "data": {"order": order[0], "items": items}})
@purchase_bp.route('/api/purchase/orders/update_status', methods=['POST'])
@login_required
def purchase_orders_update_status():
data = request.json
order_id = data.get('id')
status = data.get('status', '')
if not order_id or not status:
return jsonify({"code": 400, "msg": "参数错误"})
query("UPDATE purchase_orders SET status=%s WHERE id=%s", (status, order_id))
query("INSERT INTO dashboard_logs (user_id, action, detail) VALUES (%s, '更新订单状态', %s)",
(session['user_id'], f"订单ID:{order_id} 状态->{status}"))
return jsonify({"code": 0, "msg": "状态已更新"})
@purchase_bp.route('/api/purchase/orders/rollback', methods=['POST'])
@login_required
def purchase_orders_rollback():
data = request.json
order_id = data.get('order_id')
if not order_id:
return jsonify({"code": 400, "msg": "参数错误"})
query("DELETE FROM purchase_order_items WHERE order_id=%s", (order_id,))
query("DELETE FROM purchase_orders WHERE id=%s", (order_id,))
query("INSERT INTO dashboard_logs (user_id, action, detail) VALUES (%s, '回退订单', %s)",
(session['user_id'], f"订单ID:{order_id} 回退到待下单"))
return jsonify({"code": 0, "msg": "已回退到待下单管理"})
@purchase_bp.route('/api/purchase/orders/delete', methods=['POST'])
@login_required
def purchase_orders_delete():
data = request.json
order_id = data.get('id')
if not order_id:
return jsonify({"code": 400, "msg": "参数错误"})
query("DELETE FROM purchase_order_items WHERE order_id=%s", (order_id,))
query("DELETE FROM purchase_orders WHERE id=%s", (order_id,))
query("INSERT INTO dashboard_logs (user_id, action, detail) VALUES (%s, '删除订单', %s)",
(session['user_id'], f"订单ID:{order_id}"))
return jsonify({"code": 0, "msg": "删除成功"})
# ─── 国内订单管理 API ───
@purchase_bp.route('/api/domestic/orders/list')
@login_required
def domestic_orders_list():
"""国内订单列表"""
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 100))
search = request.args.get('search', '')
status_filter = request.args.get('status', '')
supplier_filter = request.args.get('supplier', '')
orderer_filter = request.args.get('orderer', '')
where = []
params = []
if search:
where.append("(`编码` LIKE %s OR `名称` LIKE %s OR `供货商` LIKE %s OR `下单备注` LIKE %s)")
kw = f'%{search}%'
params.extend([kw, kw, kw, kw])
if status_filter:
where.append("`状态` = %s")
params.append(status_filter)
if supplier_filter:
where.append("`供货商` = %s")
params.append(supplier_filter)
if orderer_filter:
where.append("`下单人` = %s")
params.append(orderer_filter)
where_clause = " WHERE " + " AND ".join(where) if where else ""
offset = (page - 1) * size
# 总记录数
cnt = query(f"SELECT COUNT(*) AS cnt FROM domestic_orders{where_clause}", params)
total = cnt[0]['cnt'] if cnt else 0
# 查询数据
rows = query(f"SELECT * FROM domestic_orders{where_clause} ORDER BY `计划下单时间` DESC, `日期` DESC LIMIT %s OFFSET %s",
params + [size, offset])
return jsonify({"code": 0, "data": {"rows": rows, "total": total, "page": page, "size": size}})
@purchase_bp.route('/api/domestic/orders/filters')
@login_required
def domestic_orders_filters():
"""获取筛选选项"""
suppliers = query("SELECT DISTINCT `供货商` FROM domestic_orders WHERE `供货商` IS NOT NULL AND `供货商` != '' ORDER BY `供货商`")
statuses = query("SELECT DISTINCT `状态` FROM domestic_orders WHERE `状态` IS NOT NULL AND `状态` != '' ORDER BY `状态`")
orderers = query("SELECT DISTINCT `下单人` FROM domestic_orders WHERE `下单人` IS NOT NULL AND `下单人` != '' ORDER BY `下单人`")
return jsonify({
"code": 0,
"data": {
"suppliers": [s['供货商'] for s in suppliers],
"statuses": [s['状态'] for s in statuses],
"orderers": [o['下单人'] for o in orderers]
}
})
@purchase_bp.route('/api/domestic/orders/update', methods=['POST'])
@login_required
def domestic_orders_update():
"""更新某一行字段值"""
data = request.json
row_id = data.get('id')
field = data.get('field')
value = data.get('value')
if not row_id or not field:
return jsonify({"code": 400, "msg": "参数错误"})
allowed_fields = [c[0] for c in [
("日期",1),("计划下单时间",1),("新旧品",1),("编码",1),("名称",1),
("下单备注",1),("单价",1),("数量",1),("金额",1),("供货商",1),
("仓库",1),("金蝶采购单",1),("采购员",1),("下单人",1),("进度",1),
("旺店通生产单号",1),("实际到货日期",1),("实际到货数量",1),("未到货数",1),
("预计到货日期",1),("金蝶入库",1),("状态",1),("是否已录入旺店通",1),
("采购开发",1),("是否审核价格",1),("父记录",1),("辅助1",1),
("实际到货数量辅助",1)
]]
if field not in allowed_fields:
return jsonify({"code": 400, "msg": f"不允许修改字段: {field}"})
query(f"UPDATE domestic_orders SET `{field}`=%s WHERE id=%s", (value, row_id))
return jsonify({"code": 0, "msg": "更新成功"})
@purchase_bp.route('/api/domestic/orders/add', methods=['POST'])
@login_required
def domestic_orders_add():
"""新增一行"""
data = request.json
if not data.get('编码'):
return jsonify({"code": 400, "msg": "编码不能为空"})
fields = ["编码","名称","供货商","数量","单价","金额","日期","计划下单时间","仓库","下单备注",
"新旧品","是否审核价格","金蝶采购单","采购员","下单人","进度","旺店通生产单号",
"是否已录入旺店通","采购开发","实际到货日期","实际到货数量","未到货数",
"预计到货日期","金蝶入库","状态","父记录"]
col_names = []
values = []
params = []
for f in fields:
val = data.get(f)
if val is not None:
col_names.append(f"`{f}`")
values.append(f"%s")
params.append(val)
if not col_names:
return jsonify({"code": 400, "msg": "没有可用的字段"})
sql = f"INSERT INTO domestic_orders ({', '.join(col_names)}) VALUES ({', '.join(values)})"
query(sql, params)
return jsonify({"code": 0, "msg": "添加成功"})
@purchase_bp.route('/api/domestic/orders/delete', methods=['POST'])
@login_required
def domestic_orders_delete():
"""删除一行"""
data = request.json
row_id = data.get('id')
if not row_id:
return jsonify({"code": 400, "msg": "参数错误"})
query("DELETE FROM domestic_orders WHERE id=%s", (row_id,))
return jsonify({"code": 0, "msg": "删除成功"})
# ─── 海外订单管理 API ───
@purchase_bp.route('/api/overseas/orders/list')
@login_required
def overseas_orders_list():
"""海外订单列表"""
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 100))
search = request.args.get('search', '')
status_filter = request.args.get('status', '')
supplier_filter = request.args.get('supplier', '')
orderer_filter = request.args.get('orderer', '')
where = []
params = []
if search:
where.append("(`产品编码` LIKE %s OR `货品名称` LIKE %s OR `供应商` LIKE %s OR `下单备注` LIKE %s)")
kw = f'%{search}%'
params.extend([kw, kw, kw, kw])
if status_filter:
where.append("`状态` = %s")
params.append(status_filter)
if supplier_filter:
where.append("`供应商` = %s")
params.append(supplier_filter)
if orderer_filter:
where.append("`下单人` = %s")
params.append(orderer_filter)
where_clause = " WHERE " + " AND ".join(where) if where else ""
offset = (page - 1) * size
cnt = query(f"SELECT COUNT(*) AS cnt FROM overseas_orders{where_clause}", params)
total = cnt[0]['cnt'] if cnt else 0
rows = query(f"SELECT * FROM overseas_orders{where_clause} ORDER BY `计划下单时间` DESC, `日期` DESC LIMIT %s OFFSET %s",
params + [size, offset])
return jsonify({"code": 0, "data": {"rows": rows, "total": total, "page": page, "size": size}})
@purchase_bp.route('/api/overseas/orders/filters')
@login_required
def overseas_orders_filters():
suppliers = query("SELECT DISTINCT `供应商` FROM overseas_orders WHERE `供应商` IS NOT NULL AND `供应商` != '' ORDER BY `供应商`")
statuses = query("SELECT DISTINCT `状态` FROM overseas_orders WHERE `状态` IS NOT NULL AND `状态` != '' ORDER BY `状态`")
orderers = query("SELECT DISTINCT `下单人` FROM overseas_orders WHERE `下单人` IS NOT NULL AND `下单人` != '' ORDER BY `下单人`")
return jsonify({"code": 0, "data": {"suppliers": [s['供应商'] for s in suppliers], "statuses": [s['状态'] for s in statuses], "orderers": [o['下单人'] for o in orderers]}})
@purchase_bp.route('/api/overseas/orders/update', methods=['POST'])
@login_required
def overseas_orders_update():
data = request.json
row_id = data.get('id')
field = data.get('field')
value = data.get('value')
if not row_id or not field:
return jsonify({"code": 400, "msg": "参数错误"})
allowed_fields = ['日期','计划下单时间','产品编码','单价','数量','金额','供应商','仓库','货品名称','新旧品',
'是否已做采购单','是否做入库','采购员','下单人','进度','下单备注','采购开发',
'旺店通生产单号','是否已录入到旺店通','实际到货日期','实际到货数量','未到货数',
'预计到货日期','状态','是否审核价格','辅助','实际到货数量辅助']
if field not in allowed_fields:
return jsonify({"code": 400, "msg": f"不允许修改字段: {field}"})
query(f"UPDATE overseas_orders SET `{field}`=%s WHERE id=%s", (value, row_id))
return jsonify({"code": 0, "msg": "更新成功"})
@purchase_bp.route('/api/overseas/orders/add', methods=['POST'])
@login_required
def overseas_orders_add():
data = request.json
if not data.get('产品编码'):
return jsonify({"code": 400, "msg": "产品编码不能为空"})
fields = ['产品编码','货品名称','供应商','数量','单价','金额','日期','计划下单时间','仓库','下单备注',
'新旧品','是否已做采购单','是否做入库','采购员','下单人','进度','旺店通生产单号',
'是否已录入到旺店通','采购开发','实际到货日期','实际到货数量','未到货数','预计到货日期','状态']
col_names, values, params = [], [], []
for f in fields:
val = data.get(f)
if val is not None:
col_names.append(f"`{f}`")
values.append("%s")
params.append(val)
if not col_names:
return jsonify({"code": 400, "msg": "没有可用的字段"})
query(f"INSERT INTO overseas_orders ({', '.join(col_names)}) VALUES ({', '.join(values)})", params)
return jsonify({"code": 0, "msg": "添加成功"})
@purchase_bp.route('/api/overseas/orders/delete', methods=['POST'])
@login_required
def overseas_orders_delete():
row_id = request.json.get('id')
if not row_id:
return jsonify({"code": 400, "msg": "参数错误"})
query("DELETE FROM overseas_orders WHERE id=%s", (row_id,))
return jsonify({"code": 0, "msg": "删除成功"})
@purchase_bp.route('/api/purchase/orders/update_item', methods=['POST'])
@login_required
def purchase_orders_update_item():
data = request.json
item_id = data.get('item_id')
order_qty = data.get('order_qty')
unit_price = data.get('unit_price')
if not item_id:
return jsonify({"code": 400, "msg": "参数错误"})
if order_qty is not None:
query("UPDATE purchase_order_items SET order_qty=%s WHERE id=%s", (int(order_qty), item_id))
if unit_price is not None:
query("UPDATE purchase_order_items SET unit_price=%s WHERE id=%s", (float(unit_price), item_id))
query("UPDATE purchase_order_items SET amount = order_qty * unit_price WHERE id=%s", (item_id,))
query("""
UPDATE purchase_orders o
SET o.total_qty = (SELECT COALESCE(SUM(order_qty),0) FROM purchase_order_items WHERE order_id=o.id),
o.total_amount = (SELECT COALESCE(SUM(amount),0) FROM purchase_order_items WHERE order_id=o.id)
WHERE o.id = (SELECT order_id FROM purchase_order_items WHERE id=%s)
""", (item_id,))
return jsonify({"code": 0, "msg": "更新成功"})
# ─── 包材订单管理 API ───
@purchase_bp.route('/api/packaging/orders/list')
@login_required
def packaging_orders_list():
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 100))
search = request.args.get('search', '')
status_filter = request.args.get('status', '')
supplier_filter = request.args.get('supplier', '')
orderer_filter = request.args.get('orderer', '')
where = []
params = []
if search:
where.append("(`编码` LIKE %s OR `名称` LIKE %s OR `供应商` LIKE %s OR `进度` LIKE %s)")
kw = f'%{search}%'
params.extend([kw, kw, kw, kw])
if status_filter:
where.append("`状态` = %s")
params.append(status_filter)
if supplier_filter:
where.append("`供应商` = %s")
params.append(supplier_filter)
if orderer_filter:
where.append("`采购下单人` = %s")
params.append(orderer_filter)
where_clause = " WHERE " + " AND ".join(where) if where else ""
offset = (page - 1) * size
cnt = query(f"SELECT COUNT(*) AS cnt FROM packaging_orders{where_clause}", params)
total = cnt[0]['cnt'] if cnt else 0
rows = query(f"SELECT * FROM packaging_orders{where_clause} ORDER BY `日期` DESC LIMIT %s OFFSET %s", params + [size, offset])
return jsonify({"code": 0, "data": {"rows": rows, "total": total, "page": page, "size": size}})
@purchase_bp.route('/api/packaging/orders/filters')
@login_required
def packaging_orders_filters():
suppliers = query("SELECT DISTINCT `供应商` FROM packaging_orders WHERE `供应商` IS NOT NULL AND `供应商` != '' ORDER BY `供应商`")
statuses = query("SELECT DISTINCT `状态` FROM packaging_orders WHERE `状态` IS NOT NULL AND `状态` != '' ORDER BY `状态`")
orderers = query("SELECT DISTINCT `采购下单人` FROM packaging_orders WHERE `采购下单人` IS NOT NULL AND `采购下单人` != '' ORDER BY `采购下单人`")
return jsonify({"code": 0, "data": {"suppliers": [s['供应商'] for s in suppliers], "statuses": [s['状态'] for s in statuses], "orderers": [o['采购下单人'] for o in orderers]}})
@purchase_bp.route('/api/packaging/orders/update', methods=['POST'])
@login_required
def packaging_orders_update():
data = request.json
row_id = data.get('id')
field = data.get('field')
value = data.get('value')
if not row_id or not field:
return jsonify({"code": 400, "msg": "参数错误"})
allowed = ['日期','编码','名称','单价','数量','合计','供应商','仓库到货','进度','是否已做采购单',
'是否已做入库','采购下单人','旺店通编号','是否已录入旺店通','到货日期','状态','做货天数','包材开发','下单备注']
if field not in allowed:
return jsonify({"code": 400, "msg": f"不允许修改字段: {field}"})
query(f"UPDATE packaging_orders SET `{field}`=%s WHERE id=%s", (value, row_id))
return jsonify({"code": 0, "msg": "更新成功"})
@purchase_bp.route('/api/packaging/orders/add', methods=['POST'])
@login_required
def packaging_orders_add():
data = request.json
if not data.get('编码'):
return jsonify({"code": 400, "msg": "编码不能为空"})
fields = ['编码','名称','供应商','数量','单价','合计','日期','仓库到货','进度','是否已做采购单',
'是否已做入库','采购下单人','旺店通编号','是否已录入旺店通','到货日期','状态','做货天数','包材开发']
col_names, values, params = [], [], []
for f in fields:
val = data.get(f)
if val is not None:
col_names.append(f"`{f}`")
values.append("%s")
params.append(val)
query(f"INSERT INTO packaging_orders ({', '.join(col_names)}) VALUES ({', '.join(values)})", params)
return jsonify({"code": 0, "msg": "添加成功"})
@purchase_bp.route('/api/packaging/orders/delete', methods=['POST'])
@login_required
def packaging_orders_delete():
row_id = request.json.get('id')
if not row_id:
return jsonify({"code": 400, "msg": "参数错误"})
query("DELETE FROM packaging_orders WHERE id=%s", (row_id,))
return jsonify({"code": 0, "msg": "删除成功"})
# ─── 成品提货表 API ───
@purchase_bp.route('/orders/finished')
@login_required
def orders_finished_page():
return render_template('base.html', page='orders_finished')
@purchase_bp.route('/api/finished/list')
@login_required
def finished_products_list():
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 100))
search = request.args.get('search', '')
category_filter = request.args.get('category', '')
supplier_filter = request.args.get('supplier', '')
where = []
params = []
if search:
where.append("(`货品编号` LIKE %s OR `货品名称` LIKE %s OR `供应商` LIKE %s OR `备注` LIKE %s)")
kw = f'%{search}%'
params.extend([kw, kw, kw, kw])
if category_filter:
where.append("`品类` = %s")
params.append(category_filter)
if supplier_filter:
where.append("`供应商` = %s")
params.append(supplier_filter)
where_clause = " WHERE " + " AND ".join(where) if where else ""
offset = (page - 1) * size
cnt = query(f"SELECT COUNT(*) AS cnt FROM finished_products{where_clause}", params)
total = cnt[0]['cnt'] if cnt else 0
rows = query(f"SELECT * FROM finished_products{where_clause} ORDER BY `序号` ASC LIMIT %s OFFSET %s", params + [size, offset])
return jsonify({"code": 0, "data": {"rows": rows, "total": total, "page": page, "size": size}})
@purchase_bp.route('/api/finished/filters')
@login_required
def finished_products_filters():
categories = query("SELECT DISTINCT `品类` FROM finished_products WHERE `品类` IS NOT NULL AND `品类` != '' ORDER BY `品类`")
suppliers = query("SELECT DISTINCT `供应商` FROM finished_products WHERE `供应商` IS NOT NULL AND `供应商` != '' ORDER BY `供应商`")
return jsonify({"code": 0, "data": {
"categories": [c['品类'] for c in categories],
"suppliers": [s['供应商'] for s in suppliers]
}})
@purchase_bp.route('/api/finished/orders/delete', methods=['POST'])
@login_required
def finished_products_delete():
row_id = request.json.get('id')
if not row_id:
return jsonify({"code": 400, "msg": "参数错误"})
query("DELETE FROM finished_products WHERE id=%s", (row_id,))
return jsonify({"code": 0, "msg": "删除成功"})
@purchase_bp.route('/api/purchase/rollback_order', methods=['POST'])
@login_required
def purchase_rollback_order():
"""下单回退:从国内/海外/包材订单表删除,恢复为待下单状态"""
data = request.json
product_code = data.get('product_code', '')
if not product_code:
return jsonify({"code": 400, "msg": "缺少货品编号"})
# 1. 查找已下单记录
rows = query("SELECT * FROM pending_orders WHERE product_code=%s AND status='ordered'", (product_code,))
if not rows:
return jsonify({"code": 400, "msg": "未找到已下单记录"})
row = rows[0]
# 2. 判断流向(同确认下单时的分流逻辑)
planner = (row.get('procurement_planner') or '').strip()
is_overseas = ('张莹纯' in planner)
# 3. 从国内/海外订单表删除
if is_overseas:
query("DELETE FROM overseas_orders WHERE 产品编码=%s", (product_code,))
else:
query("DELETE FROM domestic_orders WHERE 编码=%s", (product_code,))
# 4. 从包材订单表删除(根据BOM物料编码逐一删除)
bom_raw = row.get('bom_items')
if bom_raw:
import json
try:
bom_list = json.loads(bom_raw) if isinstance(bom_raw, str) else bom_raw
except:
bom_list = []
for bm in bom_list:
mat_code = bm.get('material_code', '')
if mat_code:
query("DELETE FROM packaging_orders WHERE 编码=%s", (mat_code,))
# 5. 恢复为待下单状态
query("UPDATE pending_orders SET status='pending', ordered_at=NULL WHERE product_code=%s AND status='ordered'",
(product_code,))
return jsonify({"code": 0, "msg": "已回退到待下单列表"})
@purchase_bp.route('/api/purchase/save_bom', methods=['POST'])
@login_required
def purchase_save_bom():
"""保存BOM实际下单数量到pending_orders"""
data = request.json
pc = data.get('product_code', '')
materials = data.get('materials', [])
if not pc or not materials:
return jsonify({"code": 400, "msg": "参数错误"})
rows = query("SELECT bom_items FROM pending_orders WHERE product_code=%s AND status='pending'", (pc,))
if not rows:
return jsonify({"code": 400, "msg": "暂无待下单记录,请先通过计划预测下单"})
import json
bom_raw = rows[0].get('bom_items', '[]')
try:
bom_list = json.loads(bom_raw) if isinstance(bom_raw, str) else bom_raw
except:
bom_list = []
# 将actual_quantity写入bom_items
mat_map = {}
for m in materials:
mat_map[m['material_code']] = m['actual_quantity']
for b in bom_list:
mc = b.get('material_code', '')
if mc in mat_map:
b['actual_quantity'] = int(mat_map[mc])
del mat_map[mc]
# 新增不在原BOM中的物料
for mc, aq in mat_map.items():
bom_list.append({'material_code': mc, 'material_name': '', 'quantity': 0, 'actual_quantity': aq})
query("UPDATE pending_orders SET bom_items=%s WHERE product_code=%s",
(json.dumps(bom_list, ensure_ascii=False), pc))
return jsonify({"code": 0, "msg": "保存成功"})