add finance
This commit is contained in:
parent
0a79a83ac3
commit
ab6c3a2f22
@ -161,6 +161,17 @@
|
|||||||
<a href="#/outsourcing-mgmt/wip-stock" class="dropdown-item" data-route="outsourcing-mgmt-wip-stock">委外在制库存</a>
|
<a href="#/outsourcing-mgmt/wip-stock" class="dropdown-item" data-route="outsourcing-mgmt-wip-stock">委外在制库存</a>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="topnav-item has-dropdown" data-menu="collect">
|
||||||
<span class="nav-left">
|
<span class="nav-left">
|
||||||
<span class="nav-icon" aria-hidden="true">
|
<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/purchase-demand.js?v=20260312" defer></script>
|
||||||
<script src="./js/components/customer-order.js" defer></script>
|
<script src="./js/components/customer-order.js" defer></script>
|
||||||
<script src="./js/components/reconciliation.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-orders.js" defer></script>
|
||||||
<script src="./js/components/outsourcing-material-issue.js" defer></script>
|
<script src="./js/components/outsourcing-material-issue.js" defer></script>
|
||||||
<script src="./js/components/finished-goods-receipt.js" defer></script>
|
<script src="./js/components/finished-goods-receipt.js" defer></script>
|
||||||
|
|||||||
@ -470,23 +470,57 @@ const Dashboard = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function render() {
|
async function render() {
|
||||||
const [dRes,pRes,yRes,tRes,mRes] = await Promise.allSettled([
|
// 先返回全屏loading遮罩层,阻止点击菜单栏
|
||||||
API.dashboard(),
|
const loadingHtml = `
|
||||||
API.auditPddQuiet(),
|
<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;">
|
||||||
API.auditYtQuiet(),
|
<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>
|
||||||
API.auditTxQuiet(),
|
<div style="color:var(--text-secondary);font-size:14px;">加载中...</div>
|
||||||
API.auditMtQuiet()
|
</div>
|
||||||
]);
|
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
|
||||||
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 loadData = async () => {
|
||||||
const mt = mRes.status==='fulfilled' ? mRes.value : { list: [] };
|
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 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>';
|
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){
|
if(window.__auditTimer){
|
||||||
clearInterval(window.__auditTimer);
|
clearInterval(window.__auditTimer);
|
||||||
@ -2107,8 +2141,10 @@ const Dashboard = (() => {
|
|||||||
|
|
||||||
// 刷新间隔10秒
|
// 刷新间隔10秒
|
||||||
window.__auditTimer=setInterval(refreshAll, 10000);
|
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-container" style="display:flex;flex-direction:column;height:100%;overflow-y:auto;padding-right:8px">
|
||||||
<!-- 四个指标卡片 -->
|
<!-- 四个指标卡片 -->
|
||||||
<div class="dashboard-metrics-4col" style="flex-shrink:0">
|
<div class="dashboard-metrics-4col" style="flex-shrink:0">
|
||||||
@ -2391,6 +2427,16 @@ const Dashboard = (() => {
|
|||||||
</div>
|
</div>
|
||||||
</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': '客户订单',
|
'customer-order': '客户订单',
|
||||||
'reconciliation': '对账单',
|
'reconciliation': '对账单',
|
||||||
export: '导出',
|
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;
|
return map[key] || key;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8722,6 +8722,16 @@ except Exception as e:
|
|||||||
print(f"⚠️ AI服务集成失败: {e}")
|
print(f"⚠️ AI服务集成失败: {e}")
|
||||||
print(" 智能报表功能将不可用,但其他功能正常")
|
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
|
@app.after_request
|
||||||
|
|||||||
BIN
server/data.db
BIN
server/data.db
Binary file not shown.
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