保留本地 data.db

This commit is contained in:
zzh 2026-05-14 09:57:32 +08:00
commit 03358b5426
7 changed files with 2362 additions and 23 deletions

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>韬智生产管理系统</title>
<link rel="icon" type="image/x-icon" href="./assets/icon.ico" />
<link rel="icon" type="image/x-icon" href="/icon.ico" />
<!-- Modern Professional Typography -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@ -36,7 +36,7 @@
<div class="brand">
<div class="brand-content">
<div class="brand-logo">
<img src="./assets/icon.ico" alt="Logo" style="width:100%;height:100%;object-fit:contain" />
<img src="/icon.ico" alt="Logo" style="width:100%;height:100%;object-fit:contain" />
</div>
<span class="brand-name">韬智生产管理</span>
</div>
@ -161,6 +161,17 @@
<a href="#/outsourcing-mgmt/wip-stock" class="dropdown-item" data-route="outsourcing-mgmt-wip-stock">委外在制库存</a>
</div>
</div>
<div class="topnav-item" data-menu="finance">
<a href="#/finance" class="nav-left" data-route="finance" style="text-decoration:none;color:inherit;">
<span class="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="1" x2="12" y2="23"/>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
</span>
<span class="topnav-text">财务管理</span>
</a>
</div>
<div class="topnav-item has-dropdown" data-menu="collect">
<span class="nav-left">
<span class="nav-icon" aria-hidden="true">
@ -495,6 +506,7 @@
<script src="./js/components/purchase-demand.js?v=20260312" defer></script>
<script src="./js/components/customer-order.js" defer></script>
<script src="./js/components/reconciliation.js" defer></script>
<script src="./js/components/finance.js?v=20250511-1530" defer></script>
<script src="./js/components/outsourcing-orders.js" defer></script>
<script src="./js/components/outsourcing-material-issue.js" defer></script>
<script src="./js/components/finished-goods-receipt.js" defer></script>

View File

@ -470,23 +470,57 @@ const Dashboard = (() => {
});
async function render() {
const [dRes,pRes,yRes,tRes,mRes] = await Promise.allSettled([
API.dashboard(),
API.auditPddQuiet(),
API.auditYtQuiet(),
API.auditTxQuiet(),
API.auditMtQuiet()
]);
const data = dRes.status==='fulfilled' ? dRes.value : { goodRate: '—', shipments: '—', defects: '—', badCount: '—' };
const pdd = pRes.status==='fulfilled' ? pRes.value : { list: [] };
const yt = yRes.status==='fulfilled' ? yRes.value : { list: [] };
const tx = tRes.status==='fulfilled' ? tRes.value : { list: [] };
const mt = mRes.status==='fulfilled' ? mRes.value : { list: [] };
// 先返回全屏loading遮罩层阻止点击菜单栏
const loadingHtml = `
<div id="dashboard-loading" style="position:fixed;top:0;left:0;right:0;bottom:0;background:var(--surface);z-index:9999;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;">
<div style="width:48px;height:48px;border:3px solid var(--border);border-top-color:var(--primary);border-radius:50%;animation:spin 1s linear infinite;"></div>
<div style="color:var(--text-secondary);font-size:14px;">加载中...</div>
</div>
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
`;
// 异步加载数据并渲染
const loadData = async () => {
const [dRes,pRes,yRes,tRes,mRes] = await Promise.allSettled([
API.dashboard(),
API.auditPddQuiet(),
API.auditYtQuiet(),
API.auditTxQuiet(),
API.auditMtQuiet()
]);
const data = dRes.status==='fulfilled' ? dRes.value : { goodRate: '—', shipments: '—', defects: '—', badCount: '—' };
const pdd = pRes.status==='fulfilled' ? pRes.value : { list: [] };
const yt = yRes.status==='fulfilled' ? yRes.value : { list: [] };
const tx = tRes.status==='fulfilled' ? tRes.value : { list: [] };
const mt = mRes.status==='fulfilled' ? mRes.value : { list: [] };
renderDashboardContent(data, pdd, yt, tx, mt);
};
// 启动数据加载
setTimeout(loadData, 0);
return loadingHtml;
}
// 实际的仪表盘内容渲染函数
function renderDashboardContent(data, pdd, yt, tx, mt) {
const view = document.getElementById('view');
if(!view) return;
// 先移除loading遮罩层
const loadingEl = document.getElementById('dashboard-loading');
if(loadingEl) loadingEl.remove();
// 优化:默认只显示少量数据,完整数据在点击模态框时加载
const pddList = (pdd.list||[]).slice(0, 10).map(r=>`<li><span style="display:inline-flex;align-items:center;gap:4px;min-width:180px"><img src="assets/pdd.svg" style="width:16px;height:16px" />${r.ts_cn||'—'}</span><span class="badge">${r.batch||''}</span><span class="badge">${r.mac||''}</span><span class="badge">${r.note||''}</span></li>`).join('')||'<li>暂无数据</li>';
const ytList = (yt.list||[]).slice(0, 10).map(r=>`<li><span style="display:inline-flex;align-items:center;gap:4px;min-width:180px"><img src="assets/yt.svg" style="width:16px;height:16px" />${r.ts_cn||'—'}</span><span class="badge">${r.batch||''}</span><span class="badge">${r.mac||''}</span><span class="badge">${r.note||''}</span></li>`).join('')||'<li>暂无数据</li>';
setTimeout(()=>{
// 构建HTML内容
let html = '';
// 清理旧的定时器和事件
window.__dashboardInit = () => {
// 清理旧的定时器和事件
if(window.__auditTimer){
clearInterval(window.__auditTimer);
@ -1225,7 +1259,7 @@ const Dashboard = (() => {
};
};
const drawShipmentDonutChart = (stats) => {
const drawShipmentDonutChart = async (stats) => {
const canvas = document.getElementById('shipment-donut-chart');
if(!canvas) return;
const ctx = canvas.getContext('2d');
@ -1243,6 +1277,20 @@ const Dashboard = (() => {
const items = Object.entries(byPlatform).filter(([, v]) => (v || 0) > 0);
items.sort((a, b) => (b[1] || 0) - (a[1] || 0));
// 获取机种列表以映射自定义机种名称
let platformList = [];
try {
const platformRes = await fetch('/api/shipments/platforms', {
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
}).then(r => r.ok ? r.json() : null);
if(platformRes && platformRes.list) {
platformList = platformRes.list;
}
} catch(e) {
// 忽略错误,使用默认映射
}
const platformName = (k) => {
const map = {
pdd: '拼多多',
@ -1253,7 +1301,13 @@ const Dashboard = (() => {
std: '标准版',
unknown: '未知'
};
return map[k] || k;
// 先从固定映射查找
if(map[k]) return map[k];
// 再从机种列表查找自定义机种名称
const customPlatform = platformList.find(p => p.value === k);
if(customPlatform) return customPlatform.label;
// 最后返回原始值
return k;
};
const platformColor = (k, fallbackIndex) => {
@ -1358,7 +1412,7 @@ const Dashboard = (() => {
window.__shipmentStatsTs = window.__shipmentStatsTs || 0;
const now = Date.now();
if(!force && (now - window.__shipmentStatsTs) < 60000 && window.__shipmentStatsCache) {
drawShipmentDonutChart(window.__shipmentStatsCache);
await drawShipmentDonutChart(window.__shipmentStatsCache);
return;
}
try {
@ -1369,7 +1423,7 @@ const Dashboard = (() => {
if(res && res.ok) {
window.__shipmentStatsCache = res;
window.__shipmentStatsTs = now;
drawShipmentDonutChart(res);
await drawShipmentDonutChart(res);
}
} catch(e) {
}
@ -2087,8 +2141,10 @@ const Dashboard = (() => {
// 刷新间隔10秒
window.__auditTimer=setInterval(refreshAll, 10000);
},0);
return `
};
// 构建HTML内容
html = `
<div class="dashboard-container" style="display:flex;flex-direction:column;height:100%;overflow-y:auto;padding-right:8px">
<!-- 四个指标卡片 -->
<div class="dashboard-metrics-4col" style="flex-shrink:0">
@ -2371,6 +2427,16 @@ const Dashboard = (() => {
</div>
</div>
`;
// 更新DOM内容替换loading动画
if(view) {
view.innerHTML = html;
}
// 触发setTimeout中的初始化逻辑确保DOM已更新
setTimeout(() => {
if(window.__dashboardInit) window.__dashboardInit();
}, 0);
}
// 跳转到良不良统计页面

File diff suppressed because it is too large Load Diff

View File

@ -92,7 +92,24 @@ const Router = (() => {
'customer-order': '客户订单',
'reconciliation': '对账单',
export: '导出',
settings: '设置'
settings: '设置',
finance: '财务管理',
'customer-rec': '客户对账',
'supplier-rec': '供应商对账',
invoice: '发票管理',
outsoucing: '委外管理',
'outsourcing-mgmt': '委外管理',
'outsourcing-orders': '委外工单',
'material-issue': '委外发料',
'finished-goods-receipt': '成品入库',
'wip-stock': '委外在制库存',
collect: '采集',
test: '测试',
meituan: '美团测试',
system: '系统',
'operations-log': '操作日志',
'material-purchase': '物料采购',
'ai-report': 'AI报表'
};
return map[key] || key;
}

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>登录 - 韬智生产管理系统</title>
<link rel="icon" type="image/x-icon" href="../icon.ico" />
<link rel="icon" type="image/x-icon" href="/icon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />

View File

@ -8722,6 +8722,16 @@ except Exception as e:
print(f"⚠️ AI服务集成失败: {e}")
print(" 智能报表功能将不可用,但其他功能正常")
# 初始化财务管理路由
try:
from finance_routes import finance_bp, init_finance_tables
app.register_blueprint(finance_bp)
# 初始化财务表(如果不存在)
init_finance_tables()
print("✅ 财务管理模块已成功集成")
except Exception as e:
print(f"⚠️ 财务管理模块集成失败: {e}")
# 添加缓存头
@app.after_request

950
server/finance_routes.py Normal file
View File

@ -0,0 +1,950 @@
# -*- coding: utf-8 -*-
"""
财务管理模块API路由
包含收支管理客户对账发票管理供应商对账
"""
import os
import json
import sqlite3
from datetime import datetime, timedelta
from functools import wraps
from flask import Blueprint, request, jsonify, session
# 创建蓝图
finance_bp = Blueprint('finance', __name__, url_prefix='/api/finance')
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, 'data.db')
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def require_login(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if not session.get('user_id'):
return jsonify({'error': 'unauthorized'}), 401
return fn(*args, **kwargs)
return wrapper
def log(action, detail=''):
"""记录操作日志"""
try:
conn = get_db()
c = conn.cursor()
uid = session.get('user_id')
username = session.get('username', 'system')
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
c.execute('INSERT INTO operations_log(user_id, action, detail, ts) VALUES (?,?,?,?)',
(uid, action, detail, ts))
conn.commit()
conn.close()
except Exception as e:
print(f'Log error: {e}')
# ==================== 数据库表初始化 ====================
def init_finance_tables():
"""初始化财务管理相关表"""
conn = get_db()
c = conn.cursor()
# 收支记录表
c.execute('''CREATE TABLE IF NOT EXISTS finance_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL, -- income:收入, expense:支出
category TEXT NOT NULL, -- 类别货款运费工资租金等
amount REAL NOT NULL,
currency TEXT DEFAULT 'CNY',
date TEXT NOT NULL,
description TEXT,
related_party TEXT, -- 关联方客户/供应商名称
related_party_type TEXT, -- 关联方类型customer/supplier
order_no TEXT, -- 关联订单号
payment_method TEXT, -- 支付方式cash/transfer/alipay/wechat
status TEXT DEFAULT 'confirmed', -- confirmed/pending/cancelled
attachments TEXT, -- JSON格式附件列表
created_by INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)''')
# 客户对账表
c.execute('''CREATE TABLE IF NOT EXISTS finance_customer_reconciliation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_name TEXT NOT NULL,
customer_code TEXT,
period_start TEXT NOT NULL,
period_end TEXT NOT NULL,
opening_balance REAL DEFAULT 0, -- 期初余额
total_income REAL DEFAULT 0, -- 本期应收
total_paid REAL DEFAULT 0, -- 本期已收
closing_balance REAL DEFAULT 0, -- 期末余额
statement_status TEXT DEFAULT 'unconfirmed', -- unconfirmed/confirmed/disputed
statement_date TEXT,
confirmed_by TEXT,
confirmed_at TEXT,
notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)''')
# 供应商对账表
c.execute('''CREATE TABLE IF NOT EXISTS finance_supplier_reconciliation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
supplier_name TEXT NOT NULL,
supplier_code TEXT,
period_start TEXT NOT NULL,
period_end TEXT NOT NULL,
opening_balance REAL DEFAULT 0,
total_expense REAL DEFAULT 0, -- 本期应付
total_paid REAL DEFAULT 0, -- 本期已付
closing_balance REAL DEFAULT 0,
statement_status TEXT DEFAULT 'unconfirmed',
statement_date TEXT,
confirmed_by TEXT,
confirmed_at TEXT,
notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)''')
# 发票管理表
c.execute('''CREATE TABLE IF NOT EXISTS finance_invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_type TEXT NOT NULL, -- input:进项发票, output:销项发票
invoice_no TEXT NOT NULL UNIQUE,
invoice_code TEXT,
amount REAL NOT NULL,
tax_amount REAL,
total_amount REAL,
issue_date TEXT NOT NULL,
related_party TEXT, -- 开票方/收票方
related_party_type TEXT, -- customer/supplier
order_no TEXT,
status TEXT DEFAULT 'normal', -- normal/voided
verification_status TEXT DEFAULT 'unverified', -- unverified/verified
verification_date TEXT,
attachment_url TEXT,
notes TEXT,
created_by INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)''')
# 账期提醒配置表
c.execute('''CREATE TABLE IF NOT EXISTS finance_payment_terms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
party_type TEXT NOT NULL, -- customer/supplier
party_name TEXT NOT NULL,
party_code TEXT,
credit_limit REAL DEFAULT 0, -- 信用额度
credit_days INTEGER DEFAULT 30, -- 账期天数
warning_days INTEGER DEFAULT 7, -- 提前预警天数
current_balance REAL DEFAULT 0,
last_transaction_date TEXT,
alert_enabled INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)''')
# 创建索引
c.execute('CREATE INDEX IF NOT EXISTS idx_transactions_date ON finance_transactions(date)')
c.execute('CREATE INDEX IF NOT EXISTS idx_transactions_type ON finance_transactions(type)')
c.execute('CREATE INDEX IF NOT EXISTS idx_transactions_party ON finance_transactions(related_party)')
c.execute('CREATE INDEX IF NOT EXISTS idx_customer_rec_period ON finance_customer_reconciliation(period_start, period_end)')
c.execute('CREATE INDEX IF NOT EXISTS idx_supplier_rec_period ON finance_supplier_reconciliation(period_start, period_end)')
c.execute('CREATE INDEX IF NOT EXISTS idx_invoices_no ON finance_invoices(invoice_no)')
c.execute('CREATE INDEX IF NOT EXISTS idx_invoices_date ON finance_invoices(issue_date)')
conn.commit()
conn.close()
print("财务管理表初始化完成")
# ==================== API路由定义 ====================
# 收支管理API
@finance_bp.route('/transactions', methods=['GET'])
@require_login
def list_transactions():
"""获取收支流水列表"""
try:
conn = get_db()
c = conn.cursor()
# 获取查询参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
type_filter = request.args.get('type') # income/expense
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
category = request.args.get('category')
related_party = request.args.get('related_party')
# 构建查询
where_clauses = []
params = []
if type_filter:
where_clauses.append('type = ?')
params.append(type_filter)
if start_date:
where_clauses.append('date >= ?')
params.append(start_date)
if end_date:
where_clauses.append('date <= ?')
params.append(end_date)
if category:
where_clauses.append('category = ?')
params.append(category)
if related_party:
where_clauses.append('related_party LIKE ?')
params.append(f'%{related_party}%')
where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
# 获取总数
c.execute(f'SELECT COUNT(*) as count FROM finance_transactions WHERE {where_sql}', params)
total = c.fetchone()['count']
# 获取数据
offset = (page - 1) * per_page
c.execute(f'''
SELECT * FROM finance_transactions
WHERE {where_sql}
ORDER BY date DESC, id DESC
LIMIT ? OFFSET ?
''', params + [per_page, offset])
rows = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({
'ok': True,
'data': rows,
'pagination': {
'page': page,
'per_page': per_page,
'total': total,
'total_pages': (total + per_page - 1) // per_page
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@finance_bp.route('/transactions', methods=['POST'])
@require_login
def create_transaction():
"""创建收支记录"""
try:
data = request.get_json()
conn = get_db()
c = conn.cursor()
c.execute('''
INSERT INTO finance_transactions
(type, category, amount, currency, date, description, related_party,
related_party_type, order_no, payment_method, status, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
data.get('type'), data.get('category'), data.get('amount'),
data.get('currency', 'CNY'), data.get('date'), data.get('description'),
data.get('related_party'), data.get('related_party_type'),
data.get('order_no'), data.get('payment_method'),
data.get('status', 'confirmed'), session.get('user_id')
))
conn.commit()
new_id = c.lastrowid
conn.close()
log('finance_transaction_create', f'创建收支记录 ID:{new_id}')
return jsonify({'ok': True, 'id': new_id})
except Exception as e:
return jsonify({'error': str(e)}), 500
@finance_bp.route('/transactions/stats', methods=['GET'])
@require_login
def transaction_stats():
"""获取收支统计"""
try:
conn = get_db()
c = conn.cursor()
# 默认查询本月
today = datetime.now()
start_of_month = today.replace(day=1).strftime('%Y-%m-%d')
# 本月收入
c.execute('''
SELECT COALESCE(SUM(amount), 0) as total
FROM finance_transactions
WHERE type = 'income' AND date >= ?
''', (start_of_month,))
month_income = c.fetchone()['total']
# 本月支出
c.execute('''
SELECT COALESCE(SUM(amount), 0) as total
FROM finance_transactions
WHERE type = 'expense' AND date >= ?
''', (start_of_month,))
month_expense = c.fetchone()['total']
# 按类别统计
c.execute('''
SELECT category, type, COALESCE(SUM(amount), 0) as total
FROM finance_transactions
WHERE date >= ?
GROUP BY category, type
''', (start_of_month,))
by_category = {}
for row in c.fetchall():
cat = row['category']
if cat not in by_category:
by_category[cat] = {'income': 0, 'expense': 0}
by_category[cat][row['type']] = row['total']
conn.close()
return jsonify({
'ok': True,
'stats': {
'month_income': month_income,
'month_expense': month_expense,
'month_net': month_income - month_expense,
'by_category': by_category
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# 客户对账API - 从对账单表(reconciliations)聚合数据
@finance_bp.route('/customer-reconciliation', methods=['GET'])
@require_login
def list_customer_reconciliation():
"""获取客户对账列表 - 从对账单表聚合"""
try:
conn = get_db()
c = conn.cursor()
customer = request.args.get('customer') # 合同号/客户名称
period = request.args.get('period') # YYYY-MM不传则显示所有历史数据
merge_by_customer = request.args.get('merge') == 'true' # 是否按客户名称整合
# 如果传了period参数按月份筛选否则查询所有数据
if period:
year, month = period.split('-')
period_start = f"{year}-{month}-01"
if month == '12':
period_end = f"{int(year)+1}-01-01"
else:
period_end = f"{year}-{int(month)+1:02d}-01"
date_filter = "REPLACE(r.order_date, '/', '-') >= ? AND REPLACE(r.order_date, '/', '-') < ?"
date_params = [period_start, period_end]
period_label = f"{year}{month}"
else:
# 查询所有历史数据
date_filter = "1=1"
date_params = []
period_start = "1970-01-01" # 用于期初计算
period_end = "2099-12-31"
period_label = "全部历史"
# 查询所有客户的对账单聚合
# 通过LEFT JOIN关联customer_orders表根据contract_no=order_no获取客户名称
if merge_by_customer:
# 按客户名称整合模式:同一客户的多个合同号合并
query = f'''
SELECT
r.contract_no,
COALESCE(co.customer_name, r.contract_no) as customer_name,
COUNT(*) as order_count,
COALESCE(SUM(r.total_amount), 0) as total_amount,
COALESCE(SUM(r.quantity), 0) as total_qty,
MIN(REPLACE(r.order_date, '/', '-')) as first_order_date,
MAX(REPLACE(r.order_date, '/', '-')) as last_order_date,
GROUP_CONCAT(DISTINCT r.material_name) as materials
FROM reconciliations r
LEFT JOIN customer_orders co ON r.contract_no = co.order_no
WHERE {date_filter}
'''
params = list(date_params)
if customer:
query += ' AND (r.contract_no LIKE ? OR co.customer_name LIKE ?)'
params.extend([f'%{customer}%', f'%{customer}%'])
query += ' GROUP BY COALESCE(co.customer_name, r.contract_no) ORDER BY last_order_date DESC'
else:
# 默认分开显示模式:每个合同号单独显示
query = f'''
SELECT
r.contract_no,
COALESCE(co.customer_name, r.contract_no) as customer_name,
COUNT(*) as order_count,
COALESCE(SUM(r.total_amount), 0) as total_amount,
COALESCE(SUM(r.quantity), 0) as total_qty,
MIN(REPLACE(r.order_date, '/', '-')) as first_order_date,
MAX(REPLACE(r.order_date, '/', '-')) as last_order_date,
GROUP_CONCAT(DISTINCT r.material_name) as materials
FROM reconciliations r
LEFT JOIN customer_orders co ON r.contract_no = co.order_no
WHERE {date_filter}
'''
params = list(date_params)
if customer:
query += ' AND (r.contract_no LIKE ? OR co.customer_name LIKE ?)'
params.extend([f'%{customer}%', f'%{customer}%'])
query += ' GROUP BY r.contract_no ORDER BY last_order_date DESC'
c.execute(query, params)
rows = [dict(r) for r in c.fetchall()]
# 计算每个客户的期初余额和回款
result = []
for row in rows:
customer_name = row['customer_name']
contract_no = row['contract_no']
if merge_by_customer:
# 整合模式:获取该客户关联的所有合同号
c.execute('''
SELECT DISTINCT r.contract_no
FROM reconciliations r
LEFT JOIN customer_orders co ON r.contract_no = co.order_no
WHERE COALESCE(co.customer_name, r.contract_no) = ?
''', (customer_name,))
contract_list = [r['contract_no'] for r in c.fetchall()]
else:
# 分开显示模式:只查当前合同号
contract_list = [contract_no]
# 构建合同号占位符
if contract_list:
placeholders = ','.join(['?' for _ in contract_list])
else:
placeholders = "'NULL'" # 防止空列表
contract_list = []
if period:
# 查询历史累计应收(本月之前)
opening_query = f'''
SELECT COALESCE(SUM(total_amount), 0) as opening_balance
FROM reconciliations
WHERE contract_no IN ({placeholders}) AND REPLACE(order_date, '/', '-') < ?
'''
c.execute(opening_query, contract_list + [period_start])
opening = c.fetchone()['opening_balance'] or 0
# 查询本期回款
c.execute('''
SELECT COALESCE(SUM(amount), 0) as total_paid
FROM finance_transactions
WHERE related_party = ? AND type = 'income'
AND category = '货款' AND date >= ? AND date < ?
''', (customer_name, period_start, period_end))
paid = c.fetchone()['total_paid'] or 0
current_period_amount = row['total_amount']
else:
# 全部历史模式
opening = 0
# 查询所有历史回款
c.execute('''
SELECT COALESCE(SUM(amount), 0) as total_paid
FROM finance_transactions
WHERE related_party = ? AND type = 'income' AND category = '货款'
''', (customer_name,))
paid = c.fetchone()['total_paid'] or 0
current_period_amount = row['total_amount']
# 计算期末余额
closing = opening + current_period_amount - paid
result.append({
'contract_no': row['contract_no'],
'customer_name': row['customer_name'],
'period_label': period_label,
'order_count': row['order_count'],
'opening_balance': opening,
'total_income': current_period_amount,
'total_paid': paid,
'closing_balance': closing,
'total_qty': row['total_qty'],
'materials': row['materials'],
'first_order_date': row['first_order_date'],
'last_order_date': row['last_order_date'],
'has_period_filter': period is not None,
'is_merged': merge_by_customer
})
conn.close()
return jsonify({'ok': True, 'data': result, 'period_label': period_label})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@finance_bp.route('/customer-reconciliation/update', methods=['POST'])
@require_login
def update_customer_name():
"""更新客户订单表中的客户名称"""
try:
data = request.get_json()
order_no = data.get('order_no')
customer_name = data.get('customer_name')
if not order_no or not customer_name:
return jsonify({'error': '缺少必要参数'}), 400
conn = get_db()
c = conn.cursor()
# 检查customer_orders表中是否存在该订单号
c.execute('SELECT id FROM customer_orders WHERE order_no = ?', (order_no,))
row = c.fetchone()
if row:
# 更新现有记录
c.execute('''
UPDATE customer_orders
SET customer_name = ?, updated_at = ?
WHERE order_no = ?
''', (customer_name, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), order_no))
else:
# 插入新记录如果合同号在customer_orders中不存在
# 需要先获取一些默认字段值
c.execute('''
INSERT INTO customer_orders
(order_no, customer_name, order_date, material, quantity, unit_price, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (order_no, customer_name, datetime.now().strftime('%Y-%m-%d'),
'默认物料', 0, 0, datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
conn.commit()
conn.close()
log('update_customer_name', f'更新合同号 {order_no} 的客户名称为 {customer_name}')
return jsonify({'ok': True, 'message': '客户名称已更新'})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@finance_bp.route('/customer-reconciliation/generate', methods=['POST'])
@require_login
def generate_customer_statement():
"""生成客户对账单"""
try:
data = request.get_json()
customer_name = data.get('customer_name')
period_start = data.get('period_start')
period_end = data.get('period_end')
conn = get_db()
c = conn.cursor()
# 计算期初余额
c.execute('''
SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) as balance
FROM finance_transactions
WHERE related_party = ? AND date < ?
''', (customer_name, period_start))
opening_balance = c.fetchone()['balance']
# 计算本期应收
c.execute('''
SELECT COALESCE(SUM(amount), 0) as total
FROM finance_transactions
WHERE related_party = ? AND type='income' AND date BETWEEN ? AND ?
''', (customer_name, period_start, period_end))
total_income = c.fetchone()['total']
# 计算本期已收
c.execute('''
SELECT COALESCE(SUM(amount), 0) as total
FROM finance_transactions
WHERE related_party = ? AND type='expense' AND category='回款' AND date BETWEEN ? AND ?
''', (customer_name, period_start, period_end))
total_paid = c.fetchone()['total']
closing_balance = opening_balance + total_income - total_paid
# 保存对账单
c.execute('''
INSERT INTO finance_customer_reconciliation
(customer_name, period_start, period_end, opening_balance, total_income, total_paid, closing_balance)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (customer_name, period_start, period_end, opening_balance, total_income, total_paid, closing_balance))
conn.commit()
new_id = c.lastrowid
conn.close()
return jsonify({'ok': True, 'id': new_id})
except Exception as e:
return jsonify({'error': str(e)}), 500
# 供应商对账API
@finance_bp.route('/supplier-reconciliation', methods=['GET'])
@require_login
def list_supplier_reconciliation():
"""获取供应商对账列表"""
try:
conn = get_db()
c = conn.cursor()
supplier = request.args.get('supplier')
period = request.args.get('period')
where_clauses = []
params = []
if supplier:
where_clauses.append('supplier_name LIKE ?')
params.append(f'%{supplier}%')
if period:
where_clauses.append("strftime('%Y-%m', period_start) = ?")
params.append(period)
where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
c.execute(f'''
SELECT * FROM finance_supplier_reconciliation
WHERE {where_sql}
ORDER BY period_end DESC
''', params)
rows = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({'ok': True, 'data': rows})
except Exception as e:
return jsonify({'error': str(e)}), 500
# 发票管理API
@finance_bp.route('/invoices', methods=['GET'])
@require_login
def list_invoices():
"""获取发票列表"""
try:
conn = get_db()
c = conn.cursor()
invoice_type = request.args.get('type') # input/output
status = request.args.get('status')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
where_clauses = []
params = []
if invoice_type:
where_clauses.append('invoice_type = ?')
params.append(invoice_type)
if status:
where_clauses.append('status = ?')
params.append(status)
if start_date:
where_clauses.append('issue_date >= ?')
params.append(start_date)
if end_date:
where_clauses.append('issue_date <= ?')
params.append(end_date)
where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
c.execute(f'''
SELECT * FROM finance_invoices
WHERE {where_sql}
ORDER BY issue_date DESC
''', params)
rows = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({'ok': True, 'data': rows})
except Exception as e:
return jsonify({'error': str(e)}), 500
@finance_bp.route('/invoices', methods=['POST'])
@require_login
def create_invoice():
"""创建发票记录"""
try:
data = request.get_json()
conn = get_db()
c = conn.cursor()
c.execute('''
INSERT INTO finance_invoices
(invoice_type, invoice_no, invoice_code, amount, tax_amount, total_amount,
issue_date, related_party, related_party_type, order_no, notes, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
data.get('invoice_type'), data.get('invoice_no'), data.get('invoice_code'),
data.get('amount'), data.get('tax_amount'), data.get('total_amount'),
data.get('issue_date'), data.get('related_party'), data.get('related_party_type'),
data.get('order_no'), data.get('notes'), session.get('user_id')
))
conn.commit()
new_id = c.lastrowid
conn.close()
return jsonify({'ok': True, 'id': new_id})
except Exception as e:
return jsonify({'error': str(e)}), 500
@finance_bp.route('/invoices/stats', methods=['GET'])
@require_login
def invoice_stats():
"""获取发票统计"""
try:
conn = get_db()
c = conn.cursor()
# 默认本月
today = datetime.now()
start_of_month = today.replace(day=1).strftime('%Y-%m-%d')
# 进项发票统计
c.execute('''
SELECT COUNT(*) as count, COALESCE(SUM(total_amount), 0) as total
FROM finance_invoices
WHERE invoice_type = 'input' AND issue_date >= ? AND status = 'normal'
''', (start_of_month,))
input_data = c.fetchone()
# 销项发票统计
c.execute('''
SELECT COUNT(*) as count, COALESCE(SUM(total_amount), 0) as total
FROM finance_invoices
WHERE invoice_type = 'output' AND issue_date >= ? AND status = 'normal'
''', (start_of_month,))
output_data = c.fetchone()
conn.close()
return jsonify({
'ok': True,
'stats': {
'input': {'count': input_data['count'], 'total': input_data['total']},
'output': {'count': output_data['count'], 'total': output_data['total']}
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# 账期提醒API
@finance_bp.route('/payment-terms', methods=['GET'])
@require_login
def list_payment_terms():
"""获取账期提醒配置"""
try:
conn = get_db()
c = conn.cursor()
party_type = request.args.get('party_type') # customer/supplier
alert_only = request.args.get('alert_only', 'false') == 'true'
where_clauses = []
params = []
if party_type:
where_clauses.append('party_type = ?')
params.append(party_type)
if alert_only:
where_clauses.append('alert_enabled = 1')
where_sql = ' AND '.join(where_clauses) if where_clauses else '1=1'
c.execute(f'''
SELECT * FROM finance_payment_terms
WHERE {where_sql}
ORDER BY current_balance DESC
''', params)
rows = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({'ok': True, 'data': rows})
except Exception as e:
return jsonify({'error': str(e)}), 500
@finance_bp.route('/payment-terms/alerts', methods=['GET'])
@require_login
def get_payment_alerts():
"""获取账期预警"""
try:
conn = get_db()
c = conn.cursor()
today = datetime.now().strftime('%Y-%m-%d')
# 查找超过账期或即将到期的
c.execute('''
SELECT pt.*,
julianday(?) - julianday(pt.last_transaction_date) as days_passed,
pt.credit_days - (julianday(?) - julianday(pt.last_transaction_date)) as days_remaining
FROM finance_payment_terms pt
WHERE pt.alert_enabled = 1
AND pt.current_balance > 0
AND (julianday(?) - julianday(pt.last_transaction_date)) >= (pt.credit_days - pt.warning_days)
ORDER BY days_passed DESC
''', (today, today, today))
rows = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({'ok': True, 'data': rows})
except Exception as e:
return jsonify({'error': str(e)}), 500
# 经营统计API
@finance_bp.route('/business-stats', methods=['GET'])
@require_login
def business_stats():
"""获取经营统计"""
try:
conn = get_db()
c = conn.cursor()
# 查询日期范围
days = request.args.get('days', 30, type=int)
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
# 收支趋势(按天)
c.execute('''
SELECT date,
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as income,
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as expense
FROM finance_transactions
WHERE date BETWEEN ? AND ?
GROUP BY date
ORDER BY date
''', (start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')))
trend = [dict(r) for r in c.fetchall()]
# 总体统计
c.execute('''
SELECT
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as total_income,
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as total_expense
FROM finance_transactions
WHERE date BETWEEN ? AND ?
''', (start_date.strftime('%Y-%m-%d'), end_date.strftime('%Y-%m-%d')))
summary = dict(c.fetchone())
# 客户应收Top10
c.execute('''
SELECT related_party,
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0) as balance
FROM finance_transactions
WHERE related_party_type = 'customer'
GROUP BY related_party
HAVING balance > 0
ORDER BY balance DESC
LIMIT 10
''')
customer_receivables = [dict(r) for r in c.fetchall()]
# 供应商应付Top10
c.execute('''
SELECT related_party,
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE -amount END), 0) as balance
FROM finance_transactions
WHERE related_party_type = 'supplier'
GROUP BY related_party
HAVING balance > 0
ORDER BY balance DESC
LIMIT 10
''')
supplier_payables = [dict(r) for r in c.fetchall()]
conn.close()
return jsonify({
'ok': True,
'stats': {
'trend': trend,
'summary': summary,
'customer_receivables': customer_receivables,
'supplier_payables': supplier_payables
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# 对账统计API
@finance_bp.route('/reconciliation-stats', methods=['GET'])
@require_login
def reconciliation_stats():
"""获取对账统计"""
try:
conn = get_db()
c = conn.cursor()
# 客户对账统计
c.execute('''
SELECT
COUNT(*) as total_statements,
SUM(CASE WHEN statement_status = 'confirmed' THEN 1 ELSE 0 END) as confirmed,
SUM(CASE WHEN statement_status = 'unconfirmed' THEN 1 ELSE 0 END) as unconfirmed,
SUM(CASE WHEN statement_status = 'disputed' THEN 1 ELSE 0 END) as disputed,
COALESCE(SUM(closing_balance), 0) as total_balance
FROM finance_customer_reconciliation
WHERE period_end >= date('now', 'start of month')
''')
customer_stats = dict(c.fetchone())
# 供应商对账统计
c.execute('''
SELECT
COUNT(*) as total_statements,
SUM(CASE WHEN statement_status = 'confirmed' THEN 1 ELSE 0 END) as confirmed,
SUM(CASE WHEN statement_status = 'unconfirmed' THEN 1 ELSE 0 END) as unconfirmed,
SUM(CASE WHEN statement_status = 'disputed' THEN 1 ELSE 0 END) as disputed,
COALESCE(SUM(closing_balance), 0) as total_balance
FROM finance_supplier_reconciliation
WHERE period_end >= date('now', 'start of month')
''')
supplier_stats = dict(c.fetchone())
conn.close()
return jsonify({
'ok': True,
'stats': {
'customer': customer_stats,
'supplier': supplier_stats
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# 初始化函数
@finance_bp.route('/init', methods=['POST'])
@require_login
def init_finance():
"""初始化财务模块(仅管理员)"""
try:
if session.get('role') not in ['admin', 'superadmin']:
return jsonify({'error': 'forbidden'}), 403
init_finance_tables()
return jsonify({'ok': True, 'message': '财务管理模块初始化成功'})
except Exception as e:
return jsonify({'error': str(e)}), 500