ERP/server/finance_routes.py

951 lines
35 KiB
Python
Raw Normal View History

2026-05-11 07:57:59 +00:00
# -*- 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