2026-04-16 13:32:15 +08:00

423 lines
18 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Financial Management Module - Core Business Logic
Implements comprehensive receivables and payments management with order-level granularity
"""
import uuid
from datetime import datetime, date, timedelta
from typing import Dict, List, Optional, Tuple
from decimal import Decimal
# Import database module (following sqlor-database-module pattern)
from appPublic.jsonconfig import getConfig
from sqlor.dbp import getDBP
class FinancialManager:
"""Financial management core class"""
def __init__(self):
self.config = getConfig()
async def get_db_connection(self, org_id: str):
"""Get database connection for organization"""
dburl = self.config.get('dburl')
return await getDBP(dburl, {'org_id': org_id})
async def create_receivable_from_order(self, order_id: str, org_id: str) -> str:
"""自动立应收:按订单生成应收计划"""
dbp = await self.get_db_connection(org_id)
# 获取订单信息(假设订单管理模块已存在)
order = await dbp.select_one("orders", {"id": order_id, "org_id": org_id})
if not order:
raise ValueError(f"订单 {order_id} 不存在")
# 计算应收日期和到期日期
receivable_date = order.get("acceptance_deadline") or order.get("created_at")
if isinstance(receivable_date, str):
receivable_date = datetime.strptime(receivable_date, "%Y-%m-%d").date()
elif isinstance(receivable_date, datetime):
receivable_date = receivable_date.date()
credit_period = int(order.get("credit_period", 0)) if order.get("credit_period") else 0
due_date = receivable_date + timedelta(days=credit_period) if credit_period > 0 else None
# 创建应收记录
receivable_id = str(uuid.uuid4()).replace('-', '')
receivable_data = {
"id": receivable_id,
"order_id": order_id,
"contract_id": order["contract_id"],
"customer_id": order["customer_id"],
"receivable_amount": order["amount"],
"received_amount": Decimal("0.00"),
"receivable_date": receivable_date,
"due_date": due_date,
"credit_period": credit_period,
"status": "pending",
"sales_owner_id": order.get("owner_id"),
"org_id": org_id,
"created_at": datetime.now(),
"updated_at": datetime.now()
}
await dbp.insert("receivables", receivable_data)
return receivable_id
async def create_receipt(self, receipt_data: Dict, user_id: str, org_id: str) -> str:
"""收款录入"""
dbp = await self.get_db_connection(org_id)
# 验证关联的订单和金额
allocations = receipt_data.get("allocations", [])
total_allocated = Decimal("0.00")
for alloc in allocations:
order_id = alloc["order_id"]
allocated_amount = Decimal(str(alloc["allocated_amount"]))
# 验证订单存在且属于当前组织
order = await dbp.select_one("orders", {"id": order_id, "org_id": org_id})
if not order:
raise ValueError(f"订单 {order_id} 不存在或不属于当前组织")
# 验证应收记录存在
receivable = await dbp.select_one("receivables", {"order_id": order_id, "org_id": org_id})
if not receivable:
raise ValueError(f"订单 {order_id} 没有对应的应收记录")
# 验证分配金额不超过应收金额
remaining_amount = Decimal(str(receivable["receivable_amount"])) - Decimal(str(receivable["received_amount"]))
if allocated_amount > remaining_amount:
raise ValueError(f"订单 {order_id} 分配金额 {allocated_amount} 超过剩余应收金额 {remaining_amount}")
total_allocated += allocated_amount
# 验证总分配金额等于收款总额
total_receipt_amount = Decimal(str(receipt_data["total_amount"]))
if abs(total_allocated - total_receipt_amount) > Decimal("0.01"): # 允许小数点误差
raise ValueError(f"分配总金额 {total_allocated} 与收款总额 {total_receipt_amount} 不匹配")
# 创建收款记录
receipt_id = str(uuid.uuid4()).replace('-', '')
receipt_number = f"REC-{datetime.now().strftime('%Y%m%d')}-{receipt_id[:8].upper()}"
final_receipt_data = {
"id": receipt_id,
"receipt_number": receipt_number,
"customer_id": receipt_data["customer_id"],
"total_amount": total_receipt_amount,
"receipt_date": receipt_data["receipt_date"],
"receipt_method": receipt_data.get("receipt_method", "bank_transfer"),
"receipt_status": "processed",
"description": receipt_data.get("description"),
"created_by": user_id,
"org_id": org_id,
"created_at": datetime.now(),
"updated_at": datetime.now()
}
await dbp.insert("receipts", final_receipt_data)
# 创建收款分配记录
for alloc in allocations:
allocation_id = str(uuid.uuid4()).replace('-', '')
receivable = await dbp.select_one("receivables", {"order_id": alloc["order_id"], "org_id": org_id})
allocation_data = {
"id": allocation_id,
"receipt_id": receipt_id,
"order_id": alloc["order_id"],
"receivable_id": receivable["id"],
"allocated_amount": Decimal(str(alloc["allocated_amount"])),
"allocation_percentage": Decimal(str(alloc.get("allocation_percentage", "0"))) if alloc.get("allocation_percentage") else None,
"contract_id": receivable["contract_id"],
"org_id": org_id,
"created_at": datetime.now()
}
await dbp.insert("receipt_allocations", allocation_data)
# 更新应收记录的已收金额
new_received_amount = Decimal(str(receivable["received_amount"])) + Decimal(str(alloc["allocated_amount"]))
new_status = "completed" if new_received_amount >= Decimal(str(receivable["receivable_amount"])) else "partial"
await dbp.update(
"receivables",
{
"received_amount": new_received_amount,
"status": new_status,
"updated_at": datetime.now()
},
{"id": receivable["id"]}
)
# 如果订单已完成收款,更新订单状态并触发合同履约
if new_status == "completed":
await self._update_order_and_contract_fulfillment(alloc["order_id"], receivable["contract_id"], org_id)
# 生成财务凭证
await self._generate_financial_vouchers(receipt_id, org_id)
return receipt_id
async def _update_order_and_contract_fulfillment(self, order_id: str, contract_id: str, org_id: str):
"""更新订单状态并触发合同履约"""
dbp = await self.get_db_connection(org_id)
# 更新订单状态为已收款
await dbp.update(
"orders",
{"status": "paid", "updated_at": datetime.now()},
{"id": order_id, "org_id": org_id}
)
# 触发合同履约进度更新(这里应该调用合同管理模块)
# 简化实现:更新合同里程碑状态
milestone_type_map = {
"advance": "预付款到账",
"progress": "进度款到账",
"final": "尾款到账",
"acceptance": "验收款到账"
}
order = await dbp.select_one("orders", {"id": order_id, "org_id": org_id})
if order:
order_type = order.get("order_type", "other")
milestone_name = milestone_type_map.get(order_type, f"{order_type}到账")
# 查找对应的里程碑并标记为完成
milestones = await dbp.query(
"SELECT id FROM contract_milestones WHERE contract_id = %(contract_id)s AND milestone_name LIKE %(milestone_name)s",
{"contract_id": contract_id, "milestone_name": f"%{milestone_name}%"}
)
for milestone in milestones:
await dbp.update(
"contract_milestones",
{"status": "completed", "actual_date": datetime.now().date(), "updated_at": datetime.now()},
{"id": milestone["id"]}
)
async def _generate_financial_vouchers(self, receipt_id: str, org_id: str):
"""生成财务凭证"""
dbp = await self.get_db_connection(org_id)
# 获取收款记录
receipt = await dbp.select_one("receipts", {"id": receipt_id, "org_id": org_id})
if not receipt:
return
# 获取分配记录
allocations = await dbp.query(
"SELECT * FROM receipt_allocations WHERE receipt_id = %(receipt_id)s AND org_id = %(org_id)s",
{"receipt_id": receipt_id, "org_id": org_id}
)
for alloc in allocations:
voucher_id = str(uuid.uuid4()).replace('-', '')
voucher_number = f"VOU-{datetime.now().strftime('%Y%m%d')}-{voucher_id[:8].upper()}"
# 获取合同和订单信息用于凭证描述
contract = await dbp.select_one("contract", {"id": alloc["contract_id"], "org_id": org_id})
order = await dbp.select_one("orders", {"id": alloc["order_id"], "org_id": org_id})
contract_number = contract["contract_number"] if contract else "UNKNOWN"
order_number = order["order_number"] if order else "UNKNOWN"
description = f"合同 {contract_number} - 订单 {order_number} 收款凭证"
voucher_data = {
"id": voucher_id,
"voucher_number": voucher_number,
"voucher_type": "receipt",
"contract_id": alloc["contract_id"],
"order_id": alloc["order_id"],
"amount": alloc["allocated_amount"],
"voucher_date": receipt["receipt_date"],
"description": description,
"reference_id": receipt_id,
"org_id": org_id,
"created_at": datetime.now()
}
await dbp.insert("financial_vouchers", voucher_data)
async def get_contract_financial_summary(self, contract_id: str, org_id: str) -> Dict:
"""获取合同层面的财务数据汇总"""
dbp = await self.get_db_connection(org_id)
# 获取合同总金额
contract = await dbp.select_one("contract", {"id": contract_id, "org_id": org_id})
if not contract:
return {}
contract_amount = Decimal(str(contract["amount"]))
# 获取所有关联订单的应收和已收金额
sql = """
SELECT
SUM(r.receivable_amount) as total_receivable,
SUM(r.received_amount) as total_received
FROM receivables r
JOIN orders o ON r.order_id = o.id
WHERE o.contract_id = %(contract_id)s
AND r.org_id = %(org_id)s
"""
result = await dbp.doQuery(sql, {"contract_id": contract_id, "org_id": org_id})
if result and result[0]["total_receivable"]:
total_receivable = Decimal(str(result[0]["total_receivable"]))
total_received = Decimal(str(result[0]["total_received"])) if result[0]["total_received"] else Decimal("0.00")
total_remaining = total_receivable - total_received
completion_rate = (total_received / contract_amount * 100) if contract_amount > 0 else Decimal("0.00")
else:
total_receivable = Decimal("0.00")
total_received = Decimal("0.00")
total_remaining = Decimal("0.00")
completion_rate = Decimal("0.00")
return {
"contract_total_amount": contract_amount,
"total_receivable_amount": total_receivable,
"total_received_amount": total_received,
"total_remaining_amount": total_remaining,
"completion_rate": float(completion_rate),
"contract_id": contract_id
}
async def get_overdue_receivables(self, org_id: str, days_overdue: int = 30) -> List[Dict]:
"""获取逾期应收记录(超期指定天数)"""
dbp = await self.get_db_connection(org_id)
sql = """
SELECT
r.*,
o.order_number,
c.contract_number,
cu.name as customer_name,
u.username as sales_owner_name
FROM receivables r
JOIN orders o ON r.order_id = o.id
JOIN contract c ON r.contract_id = c.id
JOIN customers cu ON r.customer_id = cu.id
LEFT JOIN users u ON r.sales_owner_id = u.id
WHERE r.org_id = %(org_id)s
AND r.status IN ('pending', 'partial')
AND r.due_date IS NOT NULL
AND r.due_date <= DATE_SUB(CURDATE(), INTERVAL %(days)s DAY)
ORDER BY r.due_date ASC
"""
overdue_receivables = await dbp.doQuery(sql, {"org_id": org_id, "days": days_overdue})
return overdue_receivables
async def send_overdue_notifications(self, org_id: str, days_overdue: int = 30) -> int:
"""发送逾期通知给销售和财务"""
overdue_receivables = await self.get_overdue_receivables(org_id, days_overdue)
# 这里应该集成消息通知系统
# 目前只记录日志,实际应用中应发送邮件/站内信等
notification_count = len(overdue_receivables)
if notification_count > 0:
print(f"发现 {notification_count} 个逾期应收记录需要通知")
for receivable in overdue_receivables:
print(f"通知: 销售 {receivable.get('sales_owner_name', '未知')} 和财务 - "
f"合同 {receivable['contract_number']} 订单 {receivable['order_number']} "
f"逾期 {datetime.now().date() - receivable['due_date']}")
return notification_count
async def create_payment(self, payment_data: Dict, user_id: str, org_id: str) -> str:
"""创建支出记录"""
dbp = await self.get_db_connection(org_id)
# 验证关联合同的收款已核销
contract_id = payment_data["contract_id"]
contract_summary = await self.get_contract_financial_summary(contract_id, org_id)
if contract_summary.get("total_received_amount", Decimal("0.00")) <= Decimal("0.00"):
raise ValueError("关联合同没有已核销的收款,不能创建支出")
# 创建支出记录
payment_id = str(uuid.uuid4()).replace('-', '')
payment_number = f"PMT-{datetime.now().strftime('%Y%m%d')}-{payment_id[:8].upper()}"
final_payment_data = {
"id": payment_id,
"payment_number": payment_number,
"contract_id": contract_id,
"vendor_id": payment_data["vendor_id"],
"payment_amount": Decimal(str(payment_data["payment_amount"])),
"payment_date": payment_data["payment_date"],
"payment_method": payment_data.get("payment_method", "bank_transfer"),
"payment_status": "processed",
"description": payment_data.get("description"),
"approved_by": payment_data.get("approved_by"),
"created_by": user_id,
"org_id": org_id,
"created_at": datetime.now(),
"updated_at": datetime.now()
}
await dbp.insert("payments", final_payment_data)
# 生成支出凭证
await self._generate_payment_voucher(payment_id, org_id)
return payment_id
async def _generate_payment_voucher(self, payment_id: str, org_id: str):
"""生成支出凭证"""
dbp = await self.get_db_connection(org_id)
payment = await dbp.select_one("payments", {"id": payment_id, "org_id": org_id})
if not payment:
return
contract = await dbp.select_one("contract", {"id": payment["contract_id"], "org_id": org_id})
contract_number = contract["contract_number"] if contract else "UNKNOWN"
voucher_id = str(uuid.uuid4()).replace('-', '')
voucher_number = f"VOU-{datetime.now().strftime('%Y%m%d')}-{voucher_id[:8].upper()}"
description = f"合同 {contract_number} 支出凭证"
voucher_data = {
"id": voucher_id,
"voucher_number": voucher_number,
"voucher_type": "payment",
"contract_id": payment["contract_id"],
"order_id": None, # 支出通常不关联具体订单
"amount": payment["payment_amount"],
"voucher_date": payment["payment_date"],
"description": description,
"reference_id": payment_id,
"org_id": org_id,
"created_at": datetime.now()
}
await dbp.insert("financial_vouchers", voucher_data)
# Global instance
financial_manager = FinancialManager()
# Export functions
async def create_receivable_from_order(order_id: str, org_id: str) -> str:
return await financial_manager.create_receivable_from_order(order_id, org_id)
async def create_receipt(receipt_data: Dict, user_id: str, org_id: str) -> str:
return await financial_manager.create_receipt(receipt_data, user_id, org_id)
async def get_contract_financial_summary(contract_id: str, org_id: str) -> Dict:
return await financial_manager.get_contract_financial_summary(contract_id, org_id)
async def get_overdue_receivables(org_id: str, days_overdue: int = 30) -> List[Dict]:
return await financial_manager.get_overdue_receivables(org_id, days_overdue)
async def send_overdue_notifications(org_id: str, days_overdue: int = 30) -> int:
return await financial_manager.send_overdue_notifications(org_id, days_overdue)
async def create_payment(payment_data: Dict, user_id: str, org_id: str) -> str:
return await financial_manager.create_payment(payment_data, user_id, org_id)