951 lines
35 KiB
Python
951 lines
35 KiB
Python
# -*- 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
|