保留本地 data.db
This commit is contained in:
commit
03358b5426
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
// 跳转到良不良统计页面
|
||||
|
||||
1284
frontend/js/components/finance.js
Normal file
1284
frontend/js/components/finance.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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
950
server/finance_routes.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user