423 lines
18 KiB
Python
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) |