1214 lines
50 KiB
Plaintext
1214 lines
50 KiB
Plaintext
"""采购路由"""
|
||
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": "保存成功"})
|