ERP/server/finance_routes.py
2026-05-11 15:57:59 +08:00

951 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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