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