@@ -329,13 +324,13 @@
let message = data.message;
if (data.errors && data.errors.length > 0) {
message += '\n\n错误详情:\n' + data.errors.join('\n');
- API.toast(message, 'warning', 10000);
+ API.toast(message, 'warning');
} else {
- API.toast(message, 'success', 5000);
+ API.toast(message, 'success');
}
await loadReconciliations();
} else {
- API.toast(data.error || '上传失败', 'error', 5000);
+ API.toast(data.error || '上传失败', 'error');
}
} catch (err) {
console.error('上传发货单失败:', err);
@@ -346,69 +341,6 @@
});
}
- // 刷新单价按钮
- const refreshPricesBtn = document.getElementById('refresh-prices-btn');
- if (refreshPricesBtn) {
- refreshPricesBtn.addEventListener('click', async () => {
- if (!confirm('确定要从客户订单刷新所有对账单的含税单价吗?\n此操作将根据客户订单中的单价更新对账单中的单价。')) {
- return;
- }
-
- API.toast('正在刷新单价...', 'info');
-
- try {
- const res = await fetch('/api/reconciliations/refresh-prices', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await res.json();
-
- if (res.ok && data.ok) {
- // 延长显示时间到15秒,支持多行显示
- API.toast(data.message, 'success', 15000);
- await loadReconciliations();
- } else {
- API.toast(data.error || '刷新单价失败', 'error', 5000);
- }
- } catch (err) {
- console.error('刷新单价失败:', err);
- API.toast('刷新单价失败', 'error');
- }
- });
- }
-
- // 修复含税金额按钮
- const fixTotalAmountBtn = document.getElementById('fix-total-amount-btn');
- if (fixTotalAmountBtn) {
- fixTotalAmountBtn.addEventListener('click', async () => {
- if (!confirm('确定要修复所有含税金额计算错误的记录吗?\n此操作将根据数量和单价重新计算含税金额。')) {
- return;
- }
-
- API.toast('正在修复含税金额...', 'info');
-
- try {
- const res = await fetch('/api/reconciliations/fix-total-amount', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
-
- const data = await res.json();
-
- if (res.ok && data.ok) {
- API.toast(data.message, 'success', 5000);
- await loadReconciliations();
- } else {
- API.toast(data.error || '修复含税金额失败', 'error', 5000);
- }
- } catch (err) {
- console.error('修复含税金额失败:', err);
- API.toast('修复含税金额失败', 'error');
- }
- });
- }
-
const quantityInput = document.getElementById('quantity');
const unitPriceInput = document.getElementById('unit-price');
const totalAmountInput = document.getElementById('total-amount');
@@ -416,56 +348,13 @@
const calculateTotal = () => {
const qty = parseFloat(quantityInput.value) || 0;
const price = parseFloat(unitPriceInput.value) || 0;
- totalAmountInput.value = (qty * price).toFixed(2);
+ const total = qty * price;
+ totalAmountInput.value = total.toFixed(1);
};
if (quantityInput) quantityInput.addEventListener('input', calculateTotal);
if (unitPriceInput) unitPriceInput.addEventListener('input', calculateTotal);
- // 获取单价按钮
- const fetchPriceBtn = document.getElementById('fetch-price-btn');
- if (fetchPriceBtn) {
- fetchPriceBtn.addEventListener('click', async () => {
- const contractNo = document.getElementById('contract-no').value.trim();
- const materialName = document.getElementById('material-name').value.trim();
-
- if (!contractNo || !materialName) {
- API.toast('请先填写合同号和物料名称', 'warning');
- return;
- }
-
- fetchPriceBtn.disabled = true;
- fetchPriceBtn.textContent = '获取中...';
-
- try {
- const res = await fetch('/api/reconciliations/fetch-price', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- contract_no: contractNo,
- material_name: materialName
- })
- });
-
- const data = await res.json();
-
- if (res.ok && data.ok) {
- document.getElementById('unit-price').value = data.unit_price;
- calculateTotal();
- API.toast(data.message, 'success');
- } else {
- API.toast(data.error || '获取单价失败', 'error');
- }
- } catch (err) {
- console.error('获取单价失败:', err);
- API.toast('获取单价失败', 'error');
- } finally {
- fetchPriceBtn.disabled = false;
- fetchPriceBtn.textContent = '获取单价';
- }
- });
- }
-
loadReconciliations();
}, 100);
diff --git a/server/app.py b/server/app.py
index 5ad94ca..c04e8ca 100644
--- a/server/app.py
+++ b/server/app.py
@@ -2,6 +2,7 @@
import os
import json
import sqlite3
+import math
from datetime import datetime
from functools import wraps
from flask import Flask, request, jsonify, session, send_from_directory, send_file
@@ -285,6 +286,12 @@ def init_db():
updated_at TEXT
)''')
+ # 添加 product_code 字段到 purchase_demand 表(如果不存在)
+ c.execute('''PRAGMA table_info(purchase_demand)''')
+ columns = [col[1] for col in c.fetchall()]
+ if 'product_code' not in columns:
+ c.execute('''ALTER TABLE purchase_demand ADD COLUMN product_code TEXT''')
+
# 采购需求清单表 - 记录计算后的采购需求
c.execute('''CREATE TABLE IF NOT EXISTS purchase_demand(
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1502,6 +1509,184 @@ def audit_diagnose():
+@app.get('/api/audit/
/debug')
+@require_login
+def audit_debug(platform):
+ """调试审计数据"""
+ try:
+ if platform not in ['pdd', 'yt', 'tx', 'mt']:
+ return jsonify({'error': '不支持的平台'})
+
+ r = get_redis()
+ r.connection_pool.connection_kwargs['socket_timeout'] = 5
+
+ # 确定Redis键名
+ redis_keys = {
+ 'pdd': ['mac_batch_audit_pdd', 'audit:pdd', 'pdd:audit'],
+ 'yt': ['mac_batch_audit_yt', 'audit:yt', 'yt:audit'],
+ 'tx': ['mac_batch_audit_tx', 'audit:tx', 'tx:audit'],
+ 'mt': ['mac_batch_audit_mt', 'audit:mt', 'mt:audit']
+ }
+
+ result = {'keys': {}, 'samples': [], 'mapping_info': {}}
+
+ # 检查审计数据键
+ for key in redis_keys[platform]:
+ try:
+ if r.exists(key):
+ t = r.type(key)
+ result['keys'][key] = {'type': t, 'exists': True}
+ if t == 'list':
+ length = r.llen(key)
+ result['keys'][key]['length'] = length
+ # 获取前3条样例
+ samples = r.lrange(key, 0, 2)
+ for i, sample in enumerate(samples):
+ parsed = parse_audit_line(sample)
+ result['samples'].append({
+ 'key': key,
+ 'index': i,
+ 'raw': sample[:200],
+ 'parsed': parsed
+ })
+ break
+ else:
+ result['keys'][key] = {'exists': False}
+ except Exception as e:
+ result['keys'][key] = {'error': str(e)}
+
+ # 检查mapping键
+ mapping_key = f'batch_sn_mapping_{platform}'
+ try:
+ if r.exists(mapping_key):
+ result['mapping_info']['exists'] = True
+ result['mapping_info']['type'] = r.type(mapping_key)
+ if r.type(mapping_key) == 'hash':
+ size = r.hlen(mapping_key)
+ result['mapping_info']['size'] = size
+
+ # 查找特定批次
+ target_batch = 'D20250422006844'
+ all_data = r.hgetall(mapping_key)
+ found_macs = [mac for mac, batch in all_data.items() if batch == target_batch]
+ result['mapping_info']['target_batch'] = target_batch
+ result['mapping_info']['found_macs'] = found_macs[:10]
+
+ # 显示一些批次号样例
+ batches = list(set(all_data.values()))[:20]
+ result['mapping_info']['batch_samples'] = batches
+ else:
+ result['mapping_info']['exists'] = False
+ except Exception as e:
+ result['mapping_info']['error'] = str(e)
+
+ return jsonify(result)
+
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+
+@app.get('/api/audit//search')
+@require_login
+def audit_search(platform):
+ """审计数据搜索API,支持按MAC地址或批次号查询"""
+ try:
+ # 验证平台参数
+ if platform not in ['pdd', 'yt', 'tx', 'mt']:
+ return jsonify({'found': False, 'message': '不支持的平台'})
+
+ # 获取查询参数
+ mac = request.args.get('mac', '').strip()
+ batch = request.args.get('batch', '').strip()
+
+ if not mac and not batch:
+ return jsonify({'found': False, 'message': '请提供MAC地址或批次号'})
+
+ r = get_redis()
+ r.connection_pool.connection_kwargs['socket_timeout'] = 5
+
+ # 确定Redis键名
+ redis_keys = {
+ 'pdd': ['mac_batch_audit_pdd', 'audit:pdd', 'pdd:audit'],
+ 'yt': ['mac_batch_audit_yt', 'audit:yt', 'yt:audit'],
+ 'tx': ['mac_batch_audit_tx', 'audit:tx', 'tx:audit'],
+ 'mt': ['mac_batch_audit_mt', 'audit:mt', 'mt:audit']
+ }
+
+ items = []
+ for key in redis_keys[platform]:
+ try:
+ if r.exists(key) and r.type(key) == 'list':
+ items = r.lrange(key, 0, -1)
+ break
+ except Exception as e:
+ log(f'audit_search_{platform}_error', f'Redis query error: {str(e)}')
+ continue
+
+ # 如果没有找到审计数据,尝试从mapping中查找
+ if not items:
+ mapping_key = f'batch_sn_mapping_{platform}'
+ if r.exists(mapping_key) and r.type(mapping_key) == 'hash':
+ try:
+ if mac:
+ # 根据MAC查找批次
+ batch_data = r.hget(mapping_key, mac)
+ if batch_data:
+ items = [json.dumps({'mac': mac, 'batch': batch_data, 'note': 'from mapping'})]
+ elif batch:
+ # 根据批次查找所有MAC
+ all_data = r.hgetall(mapping_key)
+ items = [json.dumps({'mac': k, 'batch': v, 'note': 'from mapping'})
+ for k, v in all_data.items() if v == batch]
+ except Exception:
+ pass
+
+ # 解析并匹配数据
+ matched_records = []
+ total_checked = 0
+
+ for item in items:
+ try:
+ parsed = parse_audit_line(item)
+ total_checked += 1
+
+ # 记录调试信息
+ if total_checked <= 5: # 只记录前5条
+ log(f'audit_search_debug_{platform}', f'item={item[:100]}, parsed_mac={parsed.get("mac")}, parsed_batch={parsed.get("batch")}')
+
+ # 匹配MAC地址(不区分大小写,支持带冒号和不带冒号)
+ if mac:
+ item_mac = parsed.get('mac', '').upper().replace(':', '')
+ search_mac = mac.upper().replace(':', '')
+ if item_mac == search_mac:
+ matched_records.append(parsed)
+ log(f'audit_search_match_{platform}', f'Found MAC match: {item_mac} == {search_mac}')
+ # 匹配批次号
+ elif batch:
+ if parsed.get('batch', '') == batch:
+ matched_records.append(parsed)
+ log(f'audit_search_match_{platform}', f'Found batch match: {parsed.get("batch")} == {batch}')
+ except Exception as e:
+ log(f'audit_search_parse_error_{platform}', f'Error parsing item: {str(e)}')
+ continue
+
+ if matched_records:
+ return jsonify({
+ 'found': True,
+ 'records': matched_records,
+ 'count': len(matched_records)
+ })
+ else:
+ return jsonify({
+ 'found': False,
+ 'message': f'未找到{"MAC地址" if mac else "批次号"}为 {mac or batch} 的审计记录'
+ })
+
+ except Exception as e:
+ log(f'audit_search_{platform}_error', str(e))
+ return jsonify({'found': False, 'message': '查询失败,请稍后重试'}), 500
+
+
@app.get('/api/overview')
@require_login
def overview():
@@ -5377,354 +5562,6 @@ def upload_shipment():
return jsonify({'error': f'解析发货单失败:{str(e)}'}), 500
-@app.post('/api/reconciliations/refresh-prices')
-@require_login
-@require_any_role('admin', 'superadmin')
-def refresh_reconciliation_prices():
- """从客户订单刷新对账单含税单价"""
- try:
- conn = get_db()
- c = conn.cursor()
-
- # 获取所有客户订单的单价信息
- c.execute('SELECT order_no, material, unit_price FROM customer_orders')
- customer_orders = c.fetchall()
-
- # 构建订单单价字典
- order_prices = {}
- for order in customer_orders:
- order_no = order['order_no']
- if order_no not in order_prices:
- order_prices[order_no] = {}
- # 处理物料名称(支持换行符分割的多个物料)
- materials = str(order['material']).split('\n')
- for material in materials:
- material = material.strip()
- if material:
- order_prices[order_no][material] = order['unit_price']
-
- # 获取所有对账单记录
- c.execute('SELECT id, contract_no, material_name, unit_price FROM reconciliations ORDER BY id ASC')
- reconciliations = c.fetchall()
-
- updated_count = 0
- not_found_count = 0
- changed_details = [] # 记录详细的变更信息
-
- # 计算序号(从1开始)
- for index, recon in enumerate(reconciliations, 1):
- contract_no = recon['contract_no']
- material_name = recon['material_name']
-
- # 尝试从客户订单中查找匹配的单价
- new_price = None
- match_info = ""
-
- # 策略1:精确匹配合同号
- if contract_no in order_prices:
- # 策略1.1:精确匹配物料名
- if material_name in order_prices[contract_no]:
- new_price = order_prices[contract_no][material_name]
- match_info = f"精确匹配(合同号:{contract_no}, 物料:{material_name})"
- else:
- # 策略1.2:部分匹配物料名(对账单物料名包含订单物料名)
- for order_material, price in order_prices[contract_no].items():
- if material_name in order_material:
- new_price = price
- match_info = f"部分匹配(合同号:{contract_no}, 订单物料:{order_material[:30]}...)"
- break
-
- # 策略1.3:反向部分匹配(订单物料名包含对账单物料名)
- if new_price is None:
- for order_material, price in order_prices[contract_no].items():
- if order_material in material_name:
- new_price = price
- match_info = f"反向匹配(合同号:{contract_no}, 订单物料:{order_material[:30]}...)"
- break
-
- # 策略2:如果精确匹配没找到,尝试提取物料编码进行匹配
- if new_price is None:
- # 提取对账单物料的第一部分作为关键词
- material_keywords = []
- # 按空格、换行、横线分割
- for part in material_name.replace('\n', ' ').replace('-', ' ').split():
- if part.strip() and len(part.strip()) > 3: # 只取长度大于3的部分
- material_keywords.append(part.strip())
-
- # 尝试使用关键词匹配
- if contract_no in order_prices:
- for keyword in material_keywords:
- for order_material, price in order_prices[contract_no].items():
- if keyword in order_material:
- new_price = price
- match_info = f"关键词匹配(合同号:{contract_no}, 关键词:{keyword})"
- break
- if new_price is not None:
- break
-
- # 策略3:模糊匹配合同号(处理合同号略有差异的情况)
- if new_price is None:
- for order_no, materials in order_prices.items():
- # 策略3.1:合同号互相包含
- if contract_no in order_no or order_no in contract_no:
- # 先尝试精确匹配物料
- if material_name in materials:
- new_price = materials[material_name]
- match_info = f"模糊合同号+精确物料(订单:{order_no})"
- break
- else:
- # 尝试部分匹配物料
- for order_material, price in materials.items():
- if material_name in order_material or order_material in material_name:
- new_price = price
- match_info = f"模糊合同号+部分物料(订单:{order_no})"
- break
- if new_price is not None:
- break
-
- # 策略4:特殊处理常见物料
- if new_price is None:
- # 处理飞机盒等特殊物料
- if '飞机盒' in material_name:
- new_price = 2 # 默认飞机盒单价
- match_info = "默认单价(飞机盒)"
- # 处理蓝牙模块
- elif '蓝牙' in material_name or 'WD1MK0SMD' in material_name:
- # 查找所有蓝牙模块的单价,取平均值
- bluetooth_prices = []
- for materials in order_prices.values():
- for mat, price in materials.items():
- if '蓝牙' in mat or 'WD1MK0SMD' in mat:
- bluetooth_prices.append(price)
- if bluetooth_prices:
- new_price = sum(bluetooth_prices) / len(bluetooth_prices)
- match_info = f"蓝牙模块平均单价({new_price:.2f})"
-
- # 如果找到了新单价且与当前单价不同,则更新
- if new_price is not None and abs(new_price - recon['unit_price']) > 0.01: # 避免浮点数精度问题
- c.execute('UPDATE reconciliations SET unit_price=?, total_amount=quantity*unit_price, updated_at=? WHERE id=?',
- (new_price, get_beijing_time(), recon['id']))
- updated_count += 1
-
- # 记录变更详情
- change_detail = {
- 'index': index, # 使用表格序号
- 'id': recon['id'], # 保留ID供调试使用
- 'contract_no': contract_no,
- 'material_name': material_name[:30] + '...' if len(material_name) > 30 else material_name,
- 'old_price': recon['unit_price'],
- 'new_price': new_price,
- 'match_type': match_info
- }
- changed_details.append(change_detail)
-
- print(f"更新: 序号={index}, ID={recon['id']}, 合同={contract_no}, 旧单价={recon['unit_price']}, 新单价={new_price}, 匹配方式: {match_info}")
- elif new_price is not None and abs(new_price - recon['unit_price']) <= 0.01:
- print(f"跳过: ID={recon['id']}, 单价相同({new_price})")
- elif new_price is None:
- not_found_count += 1
- print(f"未匹配: ID={recon['id']}, 合同={contract_no}, 物料={material_name[:30]}...")
-
- conn.commit()
- conn.close()
-
- log('refresh_reconciliation_prices', f'刷新单价成功,更新了 {updated_count} 条记录')
-
- # 构建详细的变更信息
- result = {
- 'ok': True,
- 'updated_count': updated_count,
- 'not_found_count': not_found_count,
- 'changed_details': changed_details, # 添加详细的变更列表
- 'message': f'成功更新 {updated_count} 条对账单的单价'
- }
-
- if not_found_count > 0:
- result['message'] += f',{not_found_count} 条记录未找到对应的客户订单单价'
-
- # 如果有更新的记录,添加详细信息到消息中
- if changed_details:
- result['message'] += '\n\n详细信息(序号为表格中的行号):'
- for i, detail in enumerate(changed_details[:10]): # 只显示前10条
- result['message'] += f'\n第{detail["index"]}行: 合同={detail["contract_no"]}, 物料={detail["material_name"]}, 单价: {detail["old_price"]} → {detail["new_price"]}'
-
- if len(changed_details) > 10:
- result['message'] += f'\n... 还有 {len(changed_details) - 10} 条记录已更新'
-
- return jsonify(result)
-
- except Exception as e:
- log('refresh_reconciliation_prices_error', str(e))
- return jsonify({'error': f'刷新单价失败:{str(e)}'}), 500
-
-
-@app.post('/api/reconciliations/fetch-price')
-@require_login
-@require_any_role('admin', 'superadmin')
-def fetch_price_from_orders():
- """根据合同号和物料名称从客户订单获取单价"""
- try:
- contract_no = request.json.get('contract_no', '').strip()
- material_name = request.json.get('material_name', '').strip()
-
- if not contract_no or not material_name:
- return jsonify({'error': '请提供合同号和物料名称'}), 400
-
- conn = get_db()
- c = conn.cursor()
-
- # 查找匹配的客户订单单价
- c.execute('SELECT order_no, material, unit_price FROM customer_orders')
- customer_orders = c.fetchall()
-
- # 构建订单单价字典
- order_prices = {}
- for order in customer_orders:
- order_no = order['order_no']
- if order_no not in order_prices:
- order_prices[order_no] = {}
- # 处理物料名称(支持换行符分割的多个物料)
- materials = str(order['material']).split('\n')
- for material in materials:
- material = material.strip()
- if material:
- order_prices[order_no][material] = order['unit_price']
-
- # 查找匹配的单价
- found_price = None
- match_info = ""
-
- # 策略1:精确匹配合同号
- if contract_no in order_prices:
- # 策略1.1:精确匹配物料名
- if material_name in order_prices[contract_no]:
- found_price = order_prices[contract_no][material_name]
- match_info = f"精确匹配(合同号:{contract_no}, 物料:{material_name})"
- else:
- # 策略1.2:部分匹配物料名
- for order_material, price in order_prices[contract_no].items():
- if material_name in order_material:
- found_price = price
- match_info = f"部分匹配(订单物料:{order_material[:30]}...)"
- break
-
- # 策略1.3:反向部分匹配
- if found_price is None:
- for order_material, price in order_prices[contract_no].items():
- if order_material in material_name:
- found_price = price
- match_info = f"反向匹配(订单物料:{order_material[:30]}...)"
- break
-
- # 策略2:关键词匹配
- if found_price is None:
- material_keywords = []
- for part in material_name.replace('\n', ' ').replace('-', ' ').split():
- if part.strip() and len(part.strip()) > 3:
- material_keywords.append(part.strip())
-
- if contract_no in order_prices:
- for keyword in material_keywords:
- for order_material, price in order_prices[contract_no].items():
- if keyword in order_material:
- found_price = price
- match_info = f"关键词匹配({keyword})"
- break
- if found_price is not None:
- break
-
- # 策略3:模糊匹配合同号
- if found_price is None:
- for order_no, materials in order_prices.items():
- if contract_no in order_no or order_no in contract_no:
- if material_name in materials:
- found_price = materials[material_name]
- match_info = f"模糊合同号匹配({order_no})"
- break
- else:
- for order_material, price in materials.items():
- if material_name in order_material or order_material in material_name:
- found_price = price
- match_info = f"模糊匹配(订单:{order_no})"
- break
- if found_price is not None:
- break
-
- # 策略4:特殊物料处理
- if found_price is None:
- if '飞机盒' in material_name:
- found_price = 2
- match_info = "默认单价(飞机盒)"
- elif '蓝牙' in material_name or 'WD1MK0SMD' in material_name:
- bluetooth_prices = []
- for materials in order_prices.values():
- for mat, price in materials.items():
- if '蓝牙' in mat or 'WD1MK0SMD' in mat:
- bluetooth_prices.append(price)
- if bluetooth_prices:
- found_price = sum(bluetooth_prices) / len(bluetooth_prices)
- match_info = f"蓝牙模块平均单价({found_price:.2f})"
-
- conn.close()
-
- if found_price is not None:
- return jsonify({
- 'ok': True,
- 'unit_price': found_price,
- 'message': f'已从客户订单获取单价:{found_price}'
- })
- else:
- return jsonify({
- 'ok': False,
- 'error': '未找到匹配的客户订单单价'
- }), 404
-
- except Exception as e:
- log('fetch_price_error', str(e))
- return jsonify({'error': f'获取单价失败:{str(e)}'}), 500
-
-
-@app.post('/api/reconciliations/fix-total-amount')
-@require_login
-@require_any_role('admin', 'superadmin')
-def fix_total_amount():
- """修复所有对账单的含税金额(根据数量和单价重新计算)"""
- try:
- conn = get_db()
- c = conn.cursor()
-
- # 获取所有记录
- c.execute('SELECT id, quantity, unit_price, total_amount FROM reconciliations')
- records = c.fetchall()
-
- fixed_count = 0
- for record in records:
- recon_id, quantity, unit_price, current_total = record
- correct_total = quantity * unit_price
-
- # 如果含税金额不正确(考虑浮点数精度)
- if abs(current_total - correct_total) > 0.01:
- c.execute('UPDATE reconciliations SET total_amount=?, updated_at=? WHERE id=?',
- (correct_total, get_beijing_time(), recon_id))
- fixed_count += 1
- print(f"修复: ID={recon_id}, 数量={quantity}, 单价={unit_price}, 旧金额={current_total}, 新金额={correct_total}")
-
- conn.commit()
- conn.close()
-
- log('fix_total_amount', f'修复含税金额成功,修复了 {fixed_count} 条记录')
-
- return jsonify({
- 'ok': True,
- 'fixed_count': fixed_count,
- 'message': f'成功修复 {fixed_count} 条记录的含税金额'
- })
-
- except Exception as e:
- log('fix_total_amount_error', str(e))
- return jsonify({'error': f'修复含税金额失败:{str(e)}'}), 500
-
-
# ==================== BOM物料清单 API ====================
@app.get('/api/bom')
@@ -6005,7 +5842,7 @@ def get_initial_stock_list():
"""获取期初库存列表"""
conn = get_db()
c = conn.cursor()
- c.execute('SELECT * FROM initial_stock ORDER BY material_code')
+ c.execute('SELECT * FROM initial_stock ORDER BY created_at ASC')
rows = c.fetchall()
conn.close()
return jsonify({'list': [dict(r) for r in rows]})
@@ -6398,6 +6235,117 @@ def calculate_purchase_demand():
})
+@app.post('/api/purchase-demand/calculate-all')
+@require_login
+@require_any_role('superadmin')
+def calculate_all_purchase_demand():
+ """计算所有产品的采购需求
+ 根据所有产品的BOM计算采购需求
+ """
+ data = request.get_json() or {}
+ order_qty = data.get('order_qty', 0)
+
+ try:
+ order_qty = int(order_qty)
+ except (ValueError, TypeError):
+ return jsonify({'error': '订单数量必须是有效整数'}), 400
+
+ if order_qty <= 0:
+ return jsonify({'error': '订单数量必须大于0'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # 获取所有产品的BOM清单
+ c.execute('SELECT DISTINCT product_code FROM bom')
+ products = c.fetchall()
+
+ if not products:
+ conn.close()
+ return jsonify({'error': '未找到任何产品的BOM清单'}), 404
+
+ # 生成需求编号
+ import math
+ now = get_beijing_time()
+ demand_no = 'PD' + datetime.now().strftime('%Y%m%d%H%M%S')
+ username = session.get('username', '')
+
+ results = []
+
+ for product in products:
+ product_code = product['product_code']
+
+ # 获取该产品的BOM清单
+ c.execute('SELECT * FROM bom WHERE product_code=?', (product_code,))
+ bom_list = c.fetchall()
+
+ for bom in bom_list:
+ material_code = bom['material_code']
+ material_name = bom['material_name']
+ unit_qty = bom['unit_qty']
+ min_package = bom['min_package'] or 1
+ unit = bom['unit']
+ supplier = bom['supplier']
+
+ # 计算总需求 = 订单数量 * 单机用量
+ total_demand = int(math.ceil(order_qty * unit_qty))
+
+ # 获取期初库存
+ c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=?', (material_code,))
+ stock_row = c.fetchone()
+ initial_stock = stock_row['stock_qty'] if stock_row else 0
+ # 如果BOM没有设置最小包装但库存表有,使用库存表的
+ if min_package <= 1 and stock_row and stock_row['stock_min_package']:
+ min_package = stock_row['stock_min_package']
+
+ # 计算净需求 = 总需求 - 期初库存
+ net_demand = total_demand - initial_stock
+ if net_demand < 0:
+ net_demand = 0
+
+ # 按最小包装计算实际采购数量
+ if net_demand > 0 and min_package > 0:
+ actual_purchase_qty = math.ceil(net_demand / min_package) * min_package
+ else:
+ actual_purchase_qty = 0
+
+ # 插入采购需求记录
+ c.execute('''INSERT INTO purchase_demand(
+ demand_no, material_code, material_name, order_qty, bom_unit_qty,
+ total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
+ unit, supplier, status, created_by, created_at, updated_at, product_code
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (
+ demand_no, material_code, material_name, order_qty, unit_qty,
+ total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
+ unit, supplier, 'pending', username, now, now, product_code
+ ))
+
+ results.append({
+ 'material_code': material_code,
+ 'material_name': material_name,
+ 'order_qty': order_qty,
+ 'bom_unit_qty': unit_qty,
+ 'total_demand': total_demand,
+ 'initial_stock': initial_stock,
+ 'net_demand': net_demand,
+ 'min_package': min_package,
+ 'actual_purchase_qty': actual_purchase_qty,
+ 'unit': unit,
+ 'supplier': supplier,
+ 'product_code': product_code // 添加产品编码用于前端分组
+ })
+
+ conn.commit()
+ conn.close()
+
+ return jsonify({
+ 'ok': True,
+ 'demand_no': demand_no,
+ 'list': results,
+ 'message': f'成功生成 {len(results)} 条采购需求'
+ })
+
+
@app.post('/api/purchase-demand/calculate-from-orders')
@require_login
@require_any_role('superadmin')
@@ -6535,48 +6483,6 @@ def calculate_purchase_demand_from_orders():
})
-@app.put('/api/purchase-demand/')
-@require_login
-@require_any_role('superadmin')
-def update_purchase_demand(demand_id):
- """更新采购需求状态"""
- data = request.get_json() or {}
-
- status = (data.get('status') or '').strip()
- remark = (data.get('remark') or '').strip()
-
- if status and status not in ['pending', 'ordered', 'received', 'completed', 'cancelled']:
- return jsonify({'error': '无效的状态'}), 400
-
- conn = get_db()
- c = conn.cursor()
-
- c.execute('SELECT id FROM purchase_demand WHERE id=?', (demand_id,))
- if not c.fetchone():
- conn.close()
- return jsonify({'error': '采购需求不存在'}), 404
-
- now = get_beijing_time()
- updates = ['updated_at=?']
- params = [now]
-
- if status:
- updates.append('status=?')
- params.append(status)
- if remark:
- updates.append('remark=?')
- params.append(remark)
-
- params.append(demand_id)
- c.execute(f'UPDATE purchase_demand SET {", ".join(updates)} WHERE id=?', params)
-
- conn.commit()
- conn.close()
-
- log('update_purchase_demand', f'需求 ID: {demand_id}')
- return jsonify({'ok': True, 'message': '采购需求更新成功'})
-
-
@app.delete('/api/purchase-demand/')
@require_login
@require_any_role('superadmin')
@@ -6599,6 +6505,109 @@ def delete_purchase_demand(demand_id):
return jsonify({'ok': True, 'message': '采购需求删除成功'})
+@app.post('/api/purchase-demand/batch-update-status')
+@require_login
+@require_any_role('superadmin')
+def batch_update_purchase_demand_status():
+ """批量更新采购需求状态"""
+ data = request.get_json() or {}
+ ids = data.get('ids', [])
+ status = data.get('status', '').strip()
+ remark = data.get('remark', '').strip()
+
+ if not ids:
+ return jsonify({'error': '请选择要更新的记录'}), 400
+
+ if not status:
+ return jsonify({'error': '请选择状态'}), 400
+
+ if status not in ['pending', 'ordered', 'received', 'completed', 'cancelled']:
+ return jsonify({'error': '无效的状态'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+ now = get_beijing_time()
+
+ # 验证ID是否存在并获取原始状态
+ placeholders = ','.join(['?' for _ in ids])
+ c.execute(f'SELECT id, status, material_code, material_name, actual_purchase_qty, net_demand FROM purchase_demand WHERE id IN ({placeholders})', ids)
+ existing_records = c.fetchall()
+
+ if not existing_records:
+ conn.close()
+ return jsonify({'error': '未找到要更新的记录'}), 404
+
+ existing_ids = [row['id'] for row in existing_records]
+
+ # 如果状态从"已下单"改为其他状态,恢复期初库存
+ if status != 'ordered':
+ for record in existing_records:
+ if record['status'] == 'ordered':
+ material_code = record['material_code']
+ material_name = record['material_name']
+ actual_purchase_qty = record['actual_purchase_qty']
+ net_demand = record['net_demand']
+
+ # 计算之前增加的库存量:实际采购 - 净需求
+ added_stock = actual_purchase_qty - net_demand
+
+ # 从期初库存表中减去之前增加的库存(恢复原值)
+ c.execute('SELECT stock_qty FROM initial_stock WHERE material_code=?', (material_code,))
+ stock_record = c.fetchone()
+
+ if stock_record:
+ current_stock = stock_record['stock_qty']
+ restored_stock = current_stock - added_stock
+
+ # 只更新期初库存表
+ c.execute('''UPDATE initial_stock
+ SET stock_qty = ?, updated_at = ?
+ WHERE material_code = ?''',
+ (restored_stock, now, material_code))
+
+ # 构建更新语句
+ updates = ['status=?', 'updated_at=?']
+ params = [status, now]
+
+ if remark:
+ updates.append('remark=?')
+ params.append(remark)
+
+ params.extend(existing_ids)
+ c.execute(f'UPDATE purchase_demand SET {", ".join(updates)} WHERE id IN ({placeholders})', params)
+
+ # 如果状态更新为已下单,更新期初库存
+ if status == 'ordered':
+ for record in existing_records:
+ # 只处理之前不是"已下单"状态的记录
+ if record['status'] != 'ordered':
+ material_code = record['material_code']
+ material_name = record['material_name']
+ actual_purchase_qty = record['actual_purchase_qty']
+ net_demand = record['net_demand']
+
+ # 计算新的期初库存:实际采购 - 净需求
+ new_initial_stock = actual_purchase_qty - net_demand
+
+ # 只更新期初库存表
+ c.execute('''INSERT OR REPLACE INTO initial_stock
+ (material_code, material_name, stock_qty, updated_at)
+ VALUES (?, ?, ?, ?)''',
+ (material_code, material_name, new_initial_stock, now))
+
+ count = c.rowcount
+ conn.commit()
+ conn.close()
+
+ log('batch_update_purchase_demand_status', f'批量更新 {count} 条采购需求状态为: {status}')
+
+ return jsonify({
+ 'ok': True,
+ 'count': count,
+ 'message': f'成功更新 {count} 条记录'
+ })
+
+
@app.post('/api/purchase-demand/batch-delete')
@require_login
@require_any_role('superadmin')
@@ -6622,6 +6631,501 @@ def batch_delete_purchase_demand():
return jsonify({'ok': True, 'count': count, 'message': f'成功删除 {count} 条采购需求'})
+@app.post('/api/purchase-demand/recalculate')
+@require_login
+@require_any_role('superadmin')
+def recalculate_purchase_demand():
+ """基于现有采购需求重新计算(更新现有记录)"""
+ data = request.get_json() or {}
+ product_code = data.get('product_code', '').strip()
+ order_qty = data.get('order_qty', 0)
+ use_initial_stock = data.get('use_initial_stock', True) # 默认使用期初库存菜单数据
+
+ if not product_code:
+ return jsonify({'error': '请选择产品'}), 400
+
+ try:
+ order_qty = int(order_qty)
+ except (ValueError, TypeError):
+ return jsonify({'error': '订单数量必须是有效整数'}), 400
+
+ if order_qty <= 0:
+ return jsonify({'error': '订单数量必须大于0'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # 获取该产品的现有采购需求数据
+ c.execute('SELECT * FROM purchase_demand WHERE product_code=?', (product_code,))
+ existing_data = c.fetchall()
+
+ if not existing_data:
+ conn.close()
+ return jsonify({'error': f'未找到产品 {product_code} 的采购需求数据'}), 404
+
+ import math
+ now = get_beijing_time()
+ username = session.get('username', '')
+
+ results = []
+
+ for item in existing_data:
+ # 使用现有的物料信息,但更新订单数量相关的计算
+ material_code = item['material_code']
+ unit_qty = item['bom_unit_qty'] or 1 # 使用原有的单机用量
+ min_package = item['min_package'] or 1
+ # 保留原有的期初库存值(从Excel导入的)
+ initial_stock = item['initial_stock'] or 0
+
+ # 计算总需求 = 新的订单数量 * 单机用量
+ total_demand = int(math.ceil(order_qty * unit_qty))
+
+ # 根据use_initial_stock参数决定是否使用期初库存菜单的数据
+ if use_initial_stock:
+ # 获取期初库存(如果库存表有值则使用库存表的,否则保留导入时的值)
+ c.execute('SELECT stock_qty, min_package as stock_min_package FROM initial_stock WHERE material_code=?', (material_code,))
+ stock_row = c.fetchone()
+ # 只有当库存表有有效值时才覆盖导入时的期初库存
+ if stock_row and stock_row['stock_qty'] is not None:
+ initial_stock = stock_row['stock_qty']
+ # 否则保持原有的initial_stock值(从Excel导入的)
+ # 如果没有设置最小包装但库存表有,使用库存表的
+ if min_package <= 1 and stock_row and stock_row['stock_min_package']:
+ min_package = stock_row['stock_min_package']
+ # 如果不使用期初库存菜单数据,则保持原有的initial_stock值(从Excel导入的)
+
+ # 计算净需求 = 总需求 - 期初库存
+ net_demand = total_demand - initial_stock
+ if net_demand < 0:
+ net_demand = 0
+
+ # 按最小包装计算实际采购数量
+ if net_demand > 0 and min_package > 0:
+ actual_purchase_qty = math.ceil(net_demand / min_package) * min_package
+ else:
+ actual_purchase_qty = 0
+
+ # 更新现有的采购需求记录
+ c.execute('''UPDATE purchase_demand SET
+ order_qty = ?,
+ total_demand = ?,
+ initial_stock = ?,
+ net_demand = ?,
+ actual_purchase_qty = ?,
+ updated_at = ?
+ WHERE id = ?''', (
+ order_qty, total_demand, initial_stock, net_demand,
+ actual_purchase_qty, now, item['id']
+ ))
+
+ results.append({
+ 'id': item['id'],
+ 'material_code': material_code,
+ 'material_name': item['material_name'],
+ 'order_qty': order_qty,
+ 'bom_unit_qty': unit_qty,
+ 'total_demand': total_demand,
+ 'initial_stock': initial_stock,
+ 'net_demand': net_demand,
+ 'min_package': min_package,
+ 'actual_purchase_qty': actual_purchase_qty,
+ 'unit': item['unit'],
+ 'supplier': item['supplier']
+ })
+
+ conn.commit()
+ conn.close()
+
+ return jsonify({
+ 'ok': True,
+ 'list': results,
+ 'message': f'成功更新 {len(results)} 条采购需求'
+ })
+
+
+@app.post('/api/bom/add-test-data')
+@require_login
+@require_any_role('superadmin')
+def add_test_bom_data():
+ """添加测试BOM数据"""
+ conn = get_db()
+ c = conn.cursor()
+
+ # 添加AP05的测试BOM数据
+ test_bom = [
+ ('AP05', 'AP05物流版', '电阻-10K', 'R001', '电阻', 1, 1, 'pcs', '供应商A'),
+ ('AP05', 'AP05物流版', '电容-100uF', 'C001', '电容', 2, 1, 'pcs', '供应商B'),
+ ('AP05', 'AP05物流版', '芯片-主控', 'IC001', '芯片', 1, 1, 'pcs', '供应商C'),
+ ]
+
+ c.executemany('INSERT OR IGNORE INTO bom (product_code, product_name, material_code, material_name, material_type, unit_qty, min_package, unit, supplier) VALUES (?,?,?,?,?,?,?,?,?)', test_bom)
+
+ conn.commit()
+ conn.close()
+
+ return jsonify({'ok': True, 'message': '已添加测试BOM数据'})
+
+
+@app.get('/api/purchase-demand/')
+@require_login
+def get_purchase_demand_detail(id):
+ """获取采购需求详情"""
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('SELECT * FROM purchase_demand WHERE id=?', (id,))
+ item = c.fetchone()
+ conn.close()
+
+ if not item:
+ return jsonify({'error': '未找到该采购需求'}), 404
+
+ return jsonify(dict(item))
+
+
+@app.put('/api/purchase-demand/')
+@require_login
+@require_any_role('superadmin')
+def update_purchase_demand(id):
+ """更新采购需求"""
+ data = request.get_json() or {}
+
+ # 获取现有记录
+ conn = get_db()
+ c = conn.cursor()
+ c.execute('SELECT * FROM purchase_demand WHERE id=?', (id,))
+ existing = c.fetchone()
+
+ if not existing:
+ conn.close()
+ return jsonify({'error': '未找到该采购需求'}), 404
+
+ # 更新字段
+ update_fields = []
+ update_values = []
+
+ # 订单数量
+ if 'order_qty' in data:
+ update_fields.append('order_qty=?')
+ update_values.append(int(data['order_qty']))
+
+ # 单机用量
+ if 'bom_unit_qty' in data:
+ update_fields.append('bom_unit_qty=?')
+ update_values.append(float(data['bom_unit_qty']))
+
+ # 期初库存
+ if 'initial_stock' in data:
+ update_fields.append('initial_stock=?')
+ update_values.append(int(data['initial_stock']))
+
+ # 最小包装
+ if 'min_package' in data:
+ update_fields.append('min_package=?')
+ update_values.append(int(data['min_package']))
+
+ # 实际采购数量
+ if 'actual_purchase_qty' in data:
+ update_fields.append('actual_purchase_qty=?')
+ update_values.append(int(data['actual_purchase_qty']))
+
+ # 单位
+ if 'unit' in data:
+ update_fields.append('unit=?')
+ update_values.append(data['unit'])
+
+ # 供应商
+ if 'supplier' in data:
+ update_fields.append('supplier=?')
+ update_values.append(data['supplier'])
+
+ # 状态
+ if 'status' in data:
+ update_fields.append('status=?')
+ update_values.append(data['status'])
+
+ # 备注
+ if 'remark' in data:
+ update_fields.append('remark=?')
+ update_values.append(data['remark'])
+
+ # 添加更新时间
+ update_fields.append('updated_at=?')
+ update_values.append(get_beijing_time())
+
+ # 如果更新了订单数量、单机用量或期初库存,需要重新计算相关字段
+ # 但是如果手动更新了实际采购数量,则不自动重新计算
+ if ('order_qty' in data or 'bom_unit_qty' in data or 'initial_stock' in data) and 'actual_purchase_qty' not in data:
+ order_qty = int(data.get('order_qty', existing['order_qty']))
+ bom_unit_qty = float(data.get('bom_unit_qty', existing['bom_unit_qty']))
+ initial_stock = int(data.get('initial_stock', existing['initial_stock']))
+
+ # 重新计算
+ total_demand = int(math.ceil(order_qty * bom_unit_qty))
+ net_demand = total_demand - initial_stock
+ if net_demand < 0:
+ net_demand = 0
+
+ min_package = int(data.get('min_package', existing['min_package']))
+ actual_purchase_qty = math.ceil(net_demand / min_package) * min_package if net_demand > 0 else 0
+
+ update_fields.extend(['total_demand=?', 'net_demand=?', 'actual_purchase_qty=?'])
+ update_values.extend([total_demand, net_demand, actual_purchase_qty])
+
+ # 处理状态变更对期初库存的影响
+ old_status = existing['status']
+ new_status = data.get('status', old_status)
+
+ # 如果状态从"已下单"改为其他状态,恢复期初库存
+ if 'status' in data and old_status == 'ordered' and new_status != 'ordered':
+ material_code = existing['material_code']
+ material_name = existing['material_name']
+ actual_purchase_qty = existing['actual_purchase_qty']
+ net_demand = existing['net_demand']
+
+ # 计算之前增加的库存量:实际采购 - 净需求
+ added_stock = actual_purchase_qty - net_demand
+
+ # 从期初库存表中减去之前增加的库存(恢复原值)
+ c.execute('SELECT stock_qty FROM initial_stock WHERE material_code=?', (material_code,))
+ stock_record = c.fetchone()
+
+ if stock_record:
+ current_stock = stock_record['stock_qty']
+ restored_stock = current_stock - added_stock
+
+ # 只更新期初库存表
+ c.execute('''UPDATE initial_stock
+ SET stock_qty = ?, updated_at = ?
+ WHERE material_code = ?''',
+ (restored_stock, get_beijing_time(), material_code))
+
+ # 执行更新
+ update_values.append(id)
+ c.execute(f'UPDATE purchase_demand SET {", ".join(update_fields)} WHERE id=?', update_values)
+
+ # 如果状态更新为已下单,更新期初库存
+ if 'status' in data and new_status == 'ordered' and old_status != 'ordered':
+ # 获取更新后的记录信息
+ c.execute('SELECT material_code, material_name, actual_purchase_qty, net_demand FROM purchase_demand WHERE id=?', (id,))
+ record = c.fetchone()
+
+ if record:
+ material_code, material_name, actual_purchase_qty, net_demand = record
+ # 计算新的期初库存:实际采购 - 净需求
+ new_initial_stock = actual_purchase_qty - net_demand
+
+ # 只更新期初库存表
+ c.execute('''INSERT OR REPLACE INTO initial_stock
+ (material_code, material_name, stock_qty, updated_at)
+ VALUES (?, ?, ?, ?)''',
+ (material_code, material_name, new_initial_stock, get_beijing_time()))
+
+ conn.commit()
+ conn.close()
+
+ log('update_purchase_demand', f'更新采购需求 ID: {id}')
+
+ return jsonify({'ok': True, 'message': '更新成功'})
+
+
+@app.post('/api/purchase-demand/sync-to-initial-stock')
+@require_login
+@require_any_role('superadmin')
+def sync_purchase_demand_to_initial_stock():
+ """将采购需求中的期初库存同步到期初库存管理"""
+ data = request.get_json() or {}
+ materials = data.get('materials', [])
+
+ if not materials:
+ return jsonify({'error': '没有要同步的数据'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ success_count = 0
+ error_count = 0
+ errors = []
+
+ for material in materials:
+ try:
+ material_code = material.get('material_code', '').strip()
+ material_name = material.get('material_name', '').strip()
+ stock_qty = int(material.get('stock_qty', 0))
+ unit = material.get('unit', 'pcs').strip()
+ min_package = int(material.get('min_package', 1))
+ supplier = material.get('supplier', '').strip()
+
+ if not material_code:
+ errors.append('物料编码不能为空')
+ error_count += 1
+ continue
+
+ # 检查是否已存在
+ c.execute('SELECT id FROM initial_stock WHERE material_code=?', (material_code,))
+ existing = c.fetchone()
+
+ now = get_beijing_time()
+
+ if existing:
+ # 更新现有记录
+ c.execute('''UPDATE initial_stock SET
+ stock_qty=?, unit=?, min_package=?, supplier=?, updated_at=?, created_at=?
+ WHERE material_code=?''', (
+ stock_qty, unit, min_package, supplier, now, now, material_code
+ ))
+ else:
+ # 插入新记录
+ c.execute('''INSERT INTO initial_stock
+ (material_code, material_name, stock_qty, unit, min_package, supplier, created_at, updated_at)
+ VALUES (?,?,?,?,?,?,?,?)''', (
+ material_code, material_name, stock_qty, unit, min_package, supplier, now, now
+ ))
+
+ success_count += 1
+
+ except Exception as e:
+ errors.append(f'物料 {material.get("material_code", "")}: {str(e)}')
+ error_count += 1
+ continue
+
+ conn.commit()
+ conn.close()
+
+ log('sync_purchase_demand_to_initial_stock', f'同步 {success_count} 个物料到期初库存')
+
+ return jsonify({
+ 'ok': True,
+ 'success_count': success_count,
+ 'error_count': error_count,
+ 'errors': errors[:10], # 只返回前10个错误
+ 'message': f'成功同步 {success_count} 个物料' + (f',{error_count} 个失败' if error_count > 0 else '')
+ })
+
+
+@app.post('/api/purchase-demand/delete-product')
+@require_login
+@require_any_role('superadmin')
+def delete_product_purchase_demand():
+ """删除指定产品的所有采购需求数据"""
+ data = request.get_json() or {}
+ product_code = data.get('product_code', '').strip()
+
+ if not product_code:
+ return jsonify({'error': '请提供产品编码'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+
+ # 先统计要删除的记录数
+ c.execute('SELECT COUNT(*) FROM purchase_demand WHERE product_code=?', (product_code,))
+ count = c.fetchone()[0]
+
+ if count == 0:
+ conn.close()
+ return jsonify({'error': f'未找到产品 {product_code} 的数据'}), 404
+
+ # 删除该产品的所有数据
+ c.execute('DELETE FROM purchase_demand WHERE product_code=?', (product_code,))
+ conn.commit()
+ conn.close()
+
+ log('delete_product_purchase_demand', f'删除产品 {product_code} 的 {count} 条采购需求')
+
+ return jsonify({
+ 'ok': True,
+ 'count': count,
+ 'message': f'成功删除产品 {product_code} 的 {count} 条数据'
+ })
+
+
+@app.post('/api/purchase-demand/import')
+@require_login
+@require_any_role('superadmin')
+def import_purchase_demand():
+ """从Excel导入采购需求清单"""
+ data = request.get_json() or {}
+ excel_data = data.get('data', [])
+ product_code = data.get('product_code', '').strip()
+
+ if not excel_data:
+ return jsonify({'error': '没有可导入的数据'}), 400
+
+ if not product_code:
+ return jsonify({'error': '请提供产品编码'}), 400
+
+ conn = get_db()
+ c = conn.cursor()
+ success_count = 0
+ error_messages = []
+
+ try:
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ username = session.get('username', 'system')
+
+ for idx, item in enumerate(excel_data):
+ try:
+ # 验证必要字段
+ model = str(item.get('model', '')).strip()
+ supplier = str(item.get('supplier', '')).strip()
+ initial_stock = int(item.get('initial_stock', 0) or 0)
+ moq = int(item.get('moq', 1) or 1)
+ per_unit_qty = int(item.get('per_unit_qty', 1) or 1)
+
+ print(f'导入第{idx+1}行: model={model}, supplier={supplier}, initial_stock={initial_stock}, moq={moq}, per_unit_qty={per_unit_qty}') # 调试日志
+
+ if not model:
+ error_messages.append(f'第{idx+1}行:型号不能为空')
+ continue
+ # 供应商可以为空,如果为空则使用默认值
+ if not supplier:
+ supplier = '待定'
+
+ # 生成需求编号
+ demand_no = f"D{now.replace('-', '').replace(' ', '').replace(':', '')}{idx+1:03d}"
+
+ # 计算实际采购数量(按MOQ向上取整)
+ actual_purchase = ((max(0, per_unit_qty - initial_stock) + moq - 1) // moq) * moq
+
+ # 插入数据
+ c.execute('''INSERT INTO purchase_demand(
+ demand_no, material_code, material_name, order_qty, bom_unit_qty,
+ total_demand, initial_stock, net_demand, min_package, actual_purchase_qty,
+ unit, supplier, status, created_by, created_at, updated_at, product_code
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (
+ demand_no, model, model, 0, per_unit_qty,
+ per_unit_qty, initial_stock, max(0, per_unit_qty - initial_stock),
+ moq, actual_purchase, 'pcs', supplier, 'pending', username, now, now, product_code
+ ))
+ success_count += 1
+
+ except Exception as e:
+ error_messages.append(f'第{idx+1}行:处理失败 - {str(e)}')
+ continue
+
+ conn.commit()
+
+ # 记录日志
+ log('import_purchase_demand', f'导入 {success_count} 条采购需求')
+
+ # 构建返回消息
+ message = f'成功导入 {success_count} 条数据'
+ if error_messages:
+ message += f',{len(error_messages)} 条失败'
+
+ return jsonify({
+ 'ok': True,
+ 'count': success_count,
+ 'message': message,
+ 'errors': error_messages[:10] # 只返回前10个错误
+ })
+
+ except Exception as e:
+ conn.rollback()
+ return jsonify({'error': f'导入失败: {str(e)}'}), 500
+ finally:
+ conn.close()
+
+
@app.errorhandler(404)
def not_found(e):
# 如果请求的是 HTML 页面(通过 Accept header 判断),返回 index.html
diff --git a/test_price_refresh.py b/test_price_refresh.py
deleted file mode 100644
index 6ef1ecb..0000000
--- a/test_price_refresh.py
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-测试对账单单价刷新功能
-"""
-
-import sqlite3
-import sys
-import os
-
-# 添加项目路径
-sys.path.append(os.path.dirname(os.path.abspath(__file__)))
-
-def test_price_refresh():
- """测试价格刷新功能"""
-
- # 连接数据库
- conn = sqlite3.connect('server/data.db')
- c = conn.cursor()
-
- print("=== 测试对账单单价刷新功能 ===\n")
-
- # 1. 检查数据库结构
- print("1. 检查数据库表结构...")
- c.execute("PRAGMA table_info(customer_orders)")
- customer_orders_columns = c.fetchall()
- print(f" customer_orders 表字段: {[col[1] for col in customer_orders_columns]}")
-
- c.execute("PRAGMA table_info(reconciliations)")
- reconciliations_columns = c.fetchall()
- print(f" reconciliations 表字段: {[col[1] for col in reconciliations_columns]}")
-
- # 2. 查看现有数据
- print("\n2. 查看现有数据...")
- c.execute("SELECT COUNT(*) FROM customer_orders")
- customer_orders_count = c.fetchone()[0]
- print(f" 客户订单记录数: {customer_orders_count}")
-
- c.execute("SELECT COUNT(*) FROM reconciliations")
- reconciliations_count = c.fetchone()[0]
- print(f" 对账单记录数: {reconciliations_count}")
-
- # 3. 显示部分示例数据
- print("\n3. 客户订单示例数据:")
- c.execute("SELECT order_no, material, unit_price FROM customer_orders LIMIT 5")
- for row in c.fetchall():
- print(f" 订单号: {row[0]}, 物料: {row[1][:30]}..., 单价: {row[2]}")
-
- print("\n4. 对账单示例数据:")
- c.execute("SELECT contract_no, material_name, unit_price FROM reconciliations LIMIT 5")
- for row in c.fetchall():
- print(f" 合同号: {row[0]}, 物料: {row[1][:30]}..., 单价: {row[2]}")
-
- # 4. 测试价格匹配逻辑
- print("\n5. 测试价格匹配逻辑...")
-
- # 获取所有客户订单
- c.execute('SELECT order_no, material, unit_price FROM customer_orders')
- customer_orders = c.fetchall()
-
- # 构建订单单价字典
- order_prices = {}
- for order in customer_orders:
- order_no = order[0]
- if order_no not in order_prices:
- order_prices[order_no] = {}
- # 处理物料名称(支持换行符分割的多个物料)
- materials = str(order[1]).split('\n')
- for material in materials:
- material = material.strip()
- if material:
- order_prices[order_no][material] = order[2]
-
- # 测试对账单匹配
- c.execute('SELECT id, contract_no, material_name, unit_price FROM reconciliations LIMIT 10')
- test_reconciliations = c.fetchall()
-
- matched_count = 0
- for recon in test_reconciliations:
- recon_id, contract_no, material_name, current_price = recon
- new_price = None
-
- # 精确匹配合同号
- if contract_no in order_prices:
- # 尝试精确匹配物料名
- if material_name in order_prices[contract_no]:
- new_price = order_prices[contract_no][material_name]
- else:
- # 尝试部分匹配
- for order_material, price in order_prices[contract_no].items():
- if material_name in order_material or order_material in material_name:
- new_price = price
- break
-
- if new_price is not None:
- print(f" ✓ 匹配成功: 对账单ID={recon_id}, 合同号={contract_no}, 当前单价={current_price}, 匹配单价={new_price}")
- matched_count += 1
- else:
- print(f" ✗ 未匹配: 对账单ID={recon_id}, 合同号={contract_no}, 物料={material_name[:30]}...")
-
- print(f"\n 测试结果: {matched_count}/{len(test_reconciliations)} 条记录成功匹配")
-
- conn.close()
-
- print("\n=== 测试完成 ===")
- print("\n功能说明:")
- print("1. 已添加 /api/reconciliations/refresh-prices 接口,用于批量刷新所有对账单单价")
- print("2. 已添加 /api/reconciliations/fetch-price 接口,用于根据合同号和物料名获取单个单价")
- print("3. 前端已添加'刷新单价'按钮,支持手动批量刷新")
- print("4. 新增/编辑对账单时,可通过'获取单价'按钮从客户订单获取最新单价")
- print("\n使用方法:")
- print("- 批量刷新: 在对账单管理页面点击'刷新单价'按钮")
- print("- 单个获取: 在新增/编辑对账单时,填写合同号和物料名称后点击'获取单价'")
-
-if __name__ == '__main__':
- test_price_refresh()