- Updated financial_core.py - Updated models/receivables.json - Added mysql.ddl.sql - Added API files: debug_receivables, receivables CRUD, test_env - Added UI files: financial_vouchers, index, payments, receipts, receivable_edit, receivables
151 lines
6.3 KiB
Python
151 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Financial Management Module - Core Business Logic
|
||
"""
|
||
|
||
import uuid
|
||
from datetime import datetime, date, timedelta
|
||
from typing import Dict, List, Optional
|
||
from decimal import Decimal
|
||
|
||
from sqlor.dbpools import DBPools
|
||
from ahserver.serverenv import ServerEnv
|
||
|
||
|
||
async def get_financial_db():
|
||
"""Get database connection"""
|
||
env = ServerEnv()
|
||
try:
|
||
dbname = env.get_module_dbname('financial_management')
|
||
except:
|
||
dbname = 'crm_db'
|
||
return DBPools().sqlorContext(dbname)
|
||
|
||
|
||
async def create_receivable_from_order(order_id: str, org_id: str) -> str:
|
||
"""自动立应收:按订单生成应收计划"""
|
||
async with await get_financial_db() as sor:
|
||
# 获取订单信息
|
||
ns = {'page': 1, 'rows': 1, 'sort': 'id'}
|
||
orders = await sor.sqlExe(
|
||
"SELECT id, contract_id, customer_id, amount, owner_id FROM orders WHERE id=${id}$ AND org_id=${org_id}$",
|
||
{'id': order_id, 'org_id': org_id}
|
||
)
|
||
if not orders:
|
||
raise ValueError(f"订单 {order_id} 不存在")
|
||
|
||
order = dict(orders[0]) if hasattr(orders[0], 'keys') else orders[0]
|
||
|
||
# 计算到期日期(使用订单创建日期+30天默认账期)
|
||
created_at = order.get('created_at')
|
||
if isinstance(created_at, str):
|
||
created_at = datetime.strptime(created_at, "%Y-%m-%d %H:%M:%S")
|
||
due_date = (created_at + timedelta(days=30)).date() if created_at else None
|
||
|
||
receivable_id = str(uuid.uuid4()).replace('-', '')[:32]
|
||
|
||
await sor.sqlExe("""
|
||
INSERT INTO receivables (id, order_id, contract_id, customer_id, receivable_amount, received_amount, due_date, status, org_id, created_at, updated_at)
|
||
VALUES (${id}$, ${order_id}$, ${contract_id}$, ${customer_id}$, ${amount}$, 0, ${due_date}$, 'pending', ${org_id}$, NOW(), NOW())
|
||
""", {
|
||
'id': receivable_id,
|
||
'order_id': order_id,
|
||
'contract_id': order.get('contract_id', ''),
|
||
'customer_id': order.get('customer_id', ''),
|
||
'amount': order.get('amount', 0),
|
||
'due_date': due_date.isoformat() if due_date else None,
|
||
'org_id': org_id
|
||
})
|
||
|
||
return receivable_id
|
||
|
||
|
||
async def create_receipt(receipt_data: Dict, user_id: str, org_id: str) -> str:
|
||
"""收款录入"""
|
||
async with await get_financial_db() as sor:
|
||
receipt_id = str(uuid.uuid4()).replace('-', '')[:32]
|
||
|
||
await sor.sqlExe("""
|
||
INSERT INTO receipts (id, customer_id, total_amount, receipt_date, receipt_method, status, org_id, created_by, created_at, updated_at)
|
||
VALUES (${id}$, ${customer_id}$, ${total_amount}$, ${receipt_date}$, ${method}$, 'processed', ${org_id}$, ${user_id}$, NOW(), NOW())
|
||
""", {
|
||
'id': receipt_id,
|
||
'customer_id': receipt_data.get('customer_id', ''),
|
||
'total_amount': receipt_data.get('total_amount', 0),
|
||
'receipt_date': receipt_data.get('receipt_date', datetime.now().date().isoformat()),
|
||
'method': receipt_data.get('receipt_method', 'bank_transfer'),
|
||
'org_id': org_id,
|
||
'user_id': user_id
|
||
})
|
||
|
||
return receipt_id
|
||
|
||
|
||
async def get_contract_financial_summary(contract_id: str, org_id: str) -> Dict:
|
||
"""获取合同层面的财务数据汇总"""
|
||
async with await get_financial_db() as sor:
|
||
ns = {'page': 1, 'rows': 50, 'sort': 'id'}
|
||
sql = """
|
||
SELECT
|
||
SUM(r.receivable_amount) as total_receivable,
|
||
SUM(r.received_amount) as total_received
|
||
FROM receivables r
|
||
WHERE r.contract_id = ${contract_id}$ AND r.org_id = ${org_id}$
|
||
"""
|
||
rows = await sor.sqlExe(sql, {'contract_id': contract_id, 'org_id': org_id})
|
||
|
||
result = {'contract_id': contract_id, 'total_receivable': 0, 'total_received': 0, 'total_remaining': 0}
|
||
if rows:
|
||
r = dict(rows[0]) if hasattr(rows[0], 'keys') else rows[0]
|
||
result['total_receivable'] = float(r.get('total_receivable', 0) or 0)
|
||
result['total_received'] = float(r.get('total_received', 0) or 0)
|
||
result['total_remaining'] = result['total_receivable'] - result['total_received']
|
||
|
||
return result
|
||
|
||
|
||
async def get_overdue_receivables(org_id: str, days_overdue: int = 30) -> List[Dict]:
|
||
"""获取逾期应收记录"""
|
||
async with await get_financial_db() as sor:
|
||
ns = {'page': 1, 'rows': 100, 'sort': 'due_date'}
|
||
sql = """
|
||
SELECT r.*, c.contract_number, o.order_id as order_number
|
||
FROM receivables r
|
||
LEFT JOIN contract c ON r.contract_id = c.id
|
||
WHERE r.org_id = ${org_id}$
|
||
AND r.status IN ('pending', 'partial')
|
||
AND r.due_date IS NOT NULL
|
||
AND r.due_date <= DATE_SUB(CURDATE(), INTERVAL ${days}$ DAY)
|
||
ORDER BY r.due_date ASC
|
||
"""
|
||
rows = await sor.sqlExe(sql, {'org_id': org_id, 'days': days_overdue})
|
||
return [dict(r) if hasattr(r, 'keys') else r for r in rows] if rows else []
|
||
|
||
|
||
async def send_overdue_notifications(org_id: str, days_overdue: int = 30) -> int:
|
||
"""发送逾期通知"""
|
||
overdue = await get_overdue_receivables(org_id, days_overdue)
|
||
return len(overdue)
|
||
|
||
|
||
async def create_payment(payment_data: Dict, user_id: str, org_id: str) -> str:
|
||
"""创建支出记录"""
|
||
async with await get_financial_db() as sor:
|
||
payment_id = str(uuid.uuid4()).replace('-', '')[:32]
|
||
|
||
await sor.sqlExe("""
|
||
INSERT INTO payments (id, contract_id, payment_amount, payment_date, payment_method, status, org_id, created_by, created_at, updated_at)
|
||
VALUES (${id}$, ${contract_id}$, ${amount}$, ${date}$, ${method}$, 'processed', ${org_id}$, ${user_id}$, NOW(), NOW())
|
||
""", {
|
||
'id': payment_id,
|
||
'contract_id': payment_data.get('contract_id', ''),
|
||
'amount': payment_data.get('payment_amount', 0),
|
||
'date': payment_data.get('payment_date', datetime.now().date().isoformat()),
|
||
'method': payment_data.get('payment_method', 'bank_transfer'),
|
||
'org_id': org_id,
|
||
'user_id': user_id
|
||
})
|
||
|
||
return payment_id
|