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

1508 lines
63 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 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,
production_order_no,
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))
bom_items = data.get('bom_items')
if bom_items is not None:
import json
bom_str = json.dumps(bom_items, ensure_ascii=False)
query("UPDATE pending_orders SET bom_items=%s WHERE product_code=%s", (bom_str, product_code))
add_operation_log('待下单编辑', f'更新 {product_code} 备注/BOM')
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', [])
bom_data_map = data.get('bom_data_map', {}) # 前端传入的BOM修改数据(若存在则优先使用)
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)
""", (
None, 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)
""", (
None, 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. 根据物料清单生成包材下单(写入包材订单表)
import logging
# 优先使用前端传入的BOM数据(用户在BOM弹窗中编辑过实际下单数量),合并数据库中完整BOM
import json
bom_raw = row.get('bom_items')
# 先从数据库加载完整BOM清单
if bom_raw:
try:
db_bom_list = json.loads(bom_raw) if isinstance(bom_raw, str) else bom_raw
except:
db_bom_list = []
else:
db_bom_list = []
# 用前端传入数据覆盖数据库数据(前端只包含有实际数量的物料,缺失的用数据库补全)
if pc in bom_data_map and bom_data_map[pc]:
front_bom = bom_data_map[pc]
# 构建code→item的映射
front_map = {}
for bm in front_bom:
code = bm.get('material_code', '')
if code:
front_map[code] = bm
# 合并:前端覆盖,数据库补全
bom_list = []
seen_codes = set()
for bm in front_bom:
code = bm.get('material_code', '')
if code and code not in seen_codes:
bom_list.append(bm)
seen_codes.add(code)
for bm in db_bom_list:
code = bm.get('material_code', '')
if code and code not in seen_codes:
bom_list.append(bm)
seen_codes.add(code)
else:
bom_list = db_bom_list
logging.warning(f'[SYNC_PACKAGING] pc={pc}, formal_qty={qty}, bom_list_len={len(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)
mat_qty = 0
else:
# quantity 和 bom_qty 都是用量字段名(来源不同)
mat_qty = float(bm.get('quantity', bm.get('bom_qty', 0)))
order_qty = int(mat_qty * qty)
print(f'[DEBUG confirm_order] mat={mat_code}, order_qty={order_qty}, act_qty={actual_qty}, mat_qty={mat_qty}, formal_qty={qty}', flush=True)
if mat_code and order_qty > 0 and not mat_code.startswith('成品_'):
mat_amount = round(order_qty * price, 2) if price else 0
# 查询物料的实际供应商(从products表取supplier_name_match)
mat_sup_rows = query("SELECT supplier_name_match FROM products WHERE product_code=%s LIMIT 1", (mat_code,))
mat_supplier = mat_sup_rows[0]['supplier_name_match'] if mat_sup_rows else row.get('supplier_name', '')
query("""INSERT INTO packaging_orders
(日期,下单时间,编码,名称,单价,数量,合计,供应商,下单备注,进度,采购下单人)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""", (
None, now_str, mat_code, mat_name,
price, order_qty, mat_amount,
mat_supplier,
bm.get('remark', ''),
'',
row.get('procurement_follower')
))
# 5. 标记为已下单(存档到下单备份管理),同时同步BOM数据(含更换稿件等)
import json
bom_str = json.dumps(bom_list, ensure_ascii=False) if bom_list else None
query("UPDATE pending_orders SET ordered_at=%s, status='ordered', bom_items=%s WHERE product_code=%s AND status='pending'",
(now_str, bom_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),("platform",1),("订单下达供应商时间",1),("total_support_days",1),("actual_order_qty",1)
]]
if field not in allowed_fields:
return jsonify({"code": 400, "msg": f"不允许修改字段: {field}"})
# 数值字段空字符串转NULL
numeric_fields = ['单价','数量','金额','未到货数','实际到货数量','实际到货数量辅助','total_support_days','actual_order_qty']
if value == '' and field in numeric_fields:
value = None
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/detail_extras')
@login_required
def domestic_orders_detail_extras():
"""国内订单详情额外数据:到货批次 + 包材信息"""
order_id = request.args.get('order_id', '')
product_code = request.args.get('product_code', '')
if not order_id:
return jsonify({"code": 400, "msg": "缺少订单ID"})
if not product_code:
# 从订单取出产品编码
order = query("SELECT `编码` FROM domestic_orders WHERE id=%s", (order_id,))
if not order:
return jsonify({"code": 404, "msg": "订单不存在"})
product_code = order[0]['编码'] or ''
if not product_code:
return jsonify({"code": 400, "msg": "订单无产品编码"})
order_id_int = int(order_id)
# 自动建表(确保 arrival_batches 表存在)
query("""CREATE TABLE IF NOT EXISTS arrival_batches (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
arrival_date DATE,
arrival_qty INT DEFAULT 0,
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)""")
# 1. 到货批次:本订单 hand-added 批次 + 同产品其他订单的历史到货
# 1a. 本订单手动添加的批次
my_batches = query("""
SELECT id, arrival_date, arrival_qty, remark
FROM arrival_batches
WHERE order_id = %s
ORDER BY arrival_date DESC, id DESC
""", (order_id_int,))
# 1b. 同一产品编码下其他订单的到货记录作为历史批次
history_batches = query("""
SELECT id, `实际到货日期` AS arrival_date, `实际到货数量` AS arrival_qty,
`状态`, `进度` AS remark
FROM domestic_orders
WHERE `编码` = %s AND id != %s
AND `实际到货日期` IS NOT NULL AND `实际到货日期` != ''
ORDER BY `实际到货日期` DESC
""", (product_code, order_id_int))
# 合并:手动批次在前,历史在后
my_batches = list(my_batches)
history_batches = list(history_batches)
marked_my = [{'from_type': 'add', **b} for b in my_batches]
# 格式化日期为 YYYY-MM-DD
for b in marked_my:
d = b.get('arrival_date')
if d and hasattr(d, 'strftime'):
b['arrival_date'] = d.strftime('%Y-%m-%d')
elif d and isinstance(d, str) and len(d) >= 10:
b['arrival_date'] = d[:10]
for b in history_batches:
bid = b['id']
unfinished = query("""
SELECT COUNT(*) AS cnt FROM domestic_orders
WHERE `编码` = %s AND id != %s
AND (`状态` IS NULL OR `状态` = '' OR `状态` = '未完结')
""", (product_code, bid))
del b['id']
del b['状态']
b['unfinished_orders_count'] = unfinished[0]['cnt'] if unfinished else 0
b['from_type'] = 'history'
# 格式化历史日期
d = b.get('arrival_date')
if d and hasattr(d, 'strftime'):
b['arrival_date'] = d.strftime('%Y-%m-%d')
elif d and isinstance(d, str) and len(d) >= 10:
b['arrival_date'] = d[:10]
marked_history = list(history_batches)
arrival_batches = marked_my + marked_history
# 2. 包材信息:BOM物料 + 包材订单
packaging_materials = []
bom_materials = query("""
SELECT b.material_code, b.material_name, b.quantity AS bom_qty
FROM bom b
WHERE b.product_code = %s
ORDER BY b.id
""", (product_code,))
for mat in bom_materials:
mcode = mat['material_code']
# 跳过内料(非包材)
mat_name = mat['material_name'] or ''
if '内料' in mat_name:
continue
# 查包材订单(取最新一条,不限制到货日期)
pkg_orders = query("""
SELECT `数量` AS order_qty, `到货日期`, `进度`, `下单时间`
FROM packaging_orders
WHERE `编码` = %s
ORDER BY id DESC
LIMIT 1
""", (mcode,))
if pkg_orders:
pkg = pkg_orders[0]
packaging_materials.append({
'material_code': mcode,
'material_name': mat_name,
'order_time': (pkg.get('下单时间') or '')[:16] if pkg.get('下单时间') else '',
'order_qty': pkg.get('order_qty') or 0,
'arrival_qty': pkg.get('order_qty') or 0, # 包材表无独立到货数量,复用下单数
'arrival_date': (pkg.get('到货日期') or '')[:10] if pkg.get('到货日期') else '',
'progress': pkg.get('进度') or '',
})
else:
# 无包材订单记录,显示BOM信息但数量为0
packaging_materials.append({
'material_code': mcode,
'material_name': mat_name,
'order_time': '',
'order_qty': 0,
'arrival_qty': 0,
'arrival_date': '',
'progress': '',
})
return jsonify({
"code": 0,
"data": {
"arrival_batches": arrival_batches,
"packaging_materials": packaging_materials
}
})
@purchase_bp.route('/api/domestic/orders/add_batch', methods=['POST'])
@login_required
def domestic_orders_add_batch():
"""添加一个到货批次"""
# 自动建表
query("""CREATE TABLE IF NOT EXISTS arrival_batches (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
arrival_date DATE,
arrival_qty INT DEFAULT 0,
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)""")
data = request.json
order_id = data.get('order_id')
arrival_date = data.get('arrival_date', '')
arrival_qty = data.get('arrival_qty', 0)
remark = data.get('remark', '')
if not order_id:
return jsonify({"code": 400, "msg": "缺少订单ID"})
row_id = query("INSERT INTO arrival_batches (order_id, arrival_date, arrival_qty, remark) VALUES (%s,%s,%s,%s)",
(order_id, arrival_date or None, int(arrival_qty) if arrival_qty else 0, remark or ''))
return jsonify({"code": 0, "msg": "添加到货批次成功", "data": {"id": row_id}})
@purchase_bp.route('/api/domestic/orders/update_batch', methods=['POST'])
@login_required
def domestic_orders_update_batch():
"""更新一个到货批次"""
data = request.json
batch_id = data.get('id')
if not batch_id:
return jsonify({"code": 400, "msg": "缺少批次ID"})
arrival_date = data.get('arrival_date', '')
arrival_qty = data.get('arrival_qty', 0)
remark = data.get('remark', '')
query("UPDATE arrival_batches SET arrival_date=%s, arrival_qty=%s, remark=%s WHERE id=%s",
(arrival_date or None, int(arrival_qty) if arrival_qty else 0, remark or '', batch_id))
return jsonify({"code": 0, "msg": "更新成功"})
@purchase_bp.route('/api/domestic/orders/delete_batch', methods=['POST'])
@login_required
def domestic_orders_delete_batch():
"""删除一个到货批次"""
data = request.json
batch_id = data.get('id')
if not batch_id:
return jsonify({"code": 400, "msg": "缺少批次ID"})
query("DELETE FROM arrival_batches WHERE id=%s", (batch_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 = ['日期','计划下单时间','产品编码','单价','数量','金额','供应商','仓库','货品名称','新旧品',
'是否已做采购单','是否做入库','采购员','下单人','进度','下单备注','采购开发',
'旺店通生产单号','是否已录入到旺店通','实际到货日期','实际到货数量','未到货数',
'预计到货日期','状态','是否审核价格','辅助','实际到货数量辅助',
'platform','订单下达供应商时间','total_support_days','lead_time']
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}"})
# 数值字段空字符串转NULL
numeric_fields = ['单价','数量','合计']
if value == '' and field in numeric_fields:
value = None
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": "保存成功"})
@purchase_bp.route('/api/purchase/backup/update_prod_no', methods=['POST'])
@login_required
def backup_update_prod_no():
"""更新下单备份的成品生产单号和包材生产单号"""
data = request.json
product_code = data.get('product_code', '')
prod_no = data.get('production_order_no', '')
if not product_code:
return jsonify({"code": 400, "msg": "参数错误"})
query("UPDATE pending_orders SET production_order_no=%s WHERE product_code=%s",
(prod_no, product_code))
add_operation_log('生产单号更新', f'{product_code}: {prod_no}')
return jsonify({"code": 0, "msg": "保存成功"})
@purchase_bp.route('/api/purchase/backup/update_mat_prod_no', methods=['POST'])
@login_required
def backup_update_mat_prod_no():
"""更新下单备份中某个包材的生产单号"""
data = request.json
product_code = data.get('product_code', '')
material_code = data.get('material_code', '')
prod_no = data.get('production_order_no', '')
if not product_code or not material_code:
return jsonify({"code": 400, "msg": "参数错误"})
import json
rows = query("SELECT bom_items FROM pending_orders WHERE product_code=%s", (product_code,))
if not rows:
return jsonify({"code": 404, "msg": "未找到"})
bom = rows[0]['bom_items']
if isinstance(bom, str):
bom = json.loads(bom)
changed = False
for item in bom:
if item.get('material_code') == material_code:
item['production_order_no'] = prod_no
changed = True
break
if changed:
query("UPDATE pending_orders SET bom_items=%s WHERE product_code=%s",
(json.dumps(bom, ensure_ascii=False), product_code))
add_operation_log('包材生产单号更新', f'{product_code}/{material_code}: {prod_no}')
return jsonify({"code": 0, "msg": "保存成功"})