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