sync: local modifications to financial_management
- 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
This commit is contained in:
parent
c87c5efaa7
commit
e3c19bc359
@ -2,422 +2,149 @@
|
||||
# -*- 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 typing import Dict, List, Optional
|
||||
from decimal import Decimal
|
||||
|
||||
# Import database module (following sqlor-database-module pattern)
|
||||
from appPublic.jsonconfig import getConfig
|
||||
from sqlor.dbp import getDBP
|
||||
from sqlor.dbpools import DBPools
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
class FinancialManager:
|
||||
"""Financial management core class"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = getConfig()
|
||||
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 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:
|
||||
async def create_receivable_from_order(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:
|
||||
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} 不存在")
|
||||
|
||||
# 计算应收日期和到期日期
|
||||
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()
|
||||
order = dict(orders[0]) if hasattr(orders[0], 'keys') else orders[0]
|
||||
|
||||
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
|
||||
# 计算到期日期(使用订单创建日期+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('-', '')
|
||||
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()
|
||||
}
|
||||
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
|
||||
})
|
||||
|
||||
await dbp.insert("receivables", receivable_data)
|
||||
return receivable_id
|
||||
|
||||
async def create_receipt(self, receipt_data: Dict, user_id: str, org_id: str) -> str:
|
||||
|
||||
async def create_receipt(receipt_data: Dict, user_id: str, org_id: str) -> str:
|
||||
"""收款录入"""
|
||||
dbp = await self.get_db_connection(org_id)
|
||||
async with await get_financial_db() as sor:
|
||||
receipt_id = str(uuid.uuid4()).replace('-', '')[:32]
|
||||
|
||||
# 验证关联的订单和金额
|
||||
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)
|
||||
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 _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:
|
||||
async def get_contract_financial_summary(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"]))
|
||||
|
||||
# 获取所有关联订单的应收和已收金额
|
||||
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
|
||||
JOIN orders o ON r.order_id = o.id
|
||||
WHERE o.contract_id = %(contract_id)s
|
||||
AND r.org_id = %(org_id)s
|
||||
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 = await dbp.doQuery(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']
|
||||
|
||||
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 result
|
||||
|
||||
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 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:
|
||||
return await financial_manager.send_overdue_notifications(org_id, days_overdue)
|
||||
"""发送逾期通知"""
|
||||
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:
|
||||
return await financial_manager.create_payment(payment_data, user_id, org_id)
|
||||
"""创建支出记录"""
|
||||
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
|
||||
|
||||
@ -1,149 +1,23 @@
|
||||
{
|
||||
"summary": {
|
||||
"tablename": "receivables",
|
||||
"label": "应收记录",
|
||||
"comment": "按订单维度的应收记录管理"
|
||||
},
|
||||
"table_name": "receivables",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "ID",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": false,
|
||||
"comments": "主键"
|
||||
},
|
||||
{
|
||||
"name": "order_id",
|
||||
"title": "订单ID",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": false,
|
||||
"comments": "关联的订单ID"
|
||||
},
|
||||
{
|
||||
"name": "contract_id",
|
||||
"title": "合同ID",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": false,
|
||||
"comments": "关联合同ID"
|
||||
},
|
||||
{
|
||||
"name": "customer_id",
|
||||
"title": "客户ID",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": false,
|
||||
"comments": "客户ID"
|
||||
},
|
||||
{
|
||||
"name": "receivable_amount",
|
||||
"title": "应收金额",
|
||||
"type": "decimal",
|
||||
"length": "15,2",
|
||||
"nullable": false,
|
||||
"comments": "订单应收金额"
|
||||
},
|
||||
{
|
||||
"name": "received_amount",
|
||||
"title": "已收金额",
|
||||
"type": "decimal",
|
||||
"length": "15,2",
|
||||
"nullable": false,
|
||||
"comments": "已收款金额,默认为0"
|
||||
},
|
||||
{
|
||||
"name": "receivable_date",
|
||||
"title": "应收日期",
|
||||
"type": "date",
|
||||
"nullable": false,
|
||||
"comments": "应收日期"
|
||||
},
|
||||
{
|
||||
"name": "due_date",
|
||||
"title": "到期日期",
|
||||
"type": "date",
|
||||
"nullable": true,
|
||||
"comments": "账期到期日期"
|
||||
},
|
||||
{
|
||||
"name": "credit_period",
|
||||
"title": "账期天数",
|
||||
"type": "long",
|
||||
"nullable": true,
|
||||
"comments": "账期天数"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": false,
|
||||
"comments": "状态: pending(待收), partial(部分收款), completed(已完成), overdue(逾期)"
|
||||
},
|
||||
{
|
||||
"name": "sales_owner_id",
|
||||
"title": "销售负责人",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": true,
|
||||
"comments": "负责该订单跟进的销售ID"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": false,
|
||||
"comments": "组织ID,用于多租户隔离"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp",
|
||||
"nullable": false,
|
||||
"comments": "创建时间"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp",
|
||||
"nullable": false,
|
||||
"comments": "更新时间"
|
||||
}
|
||||
{"name": "id", "type": "varchar(64)", "not_null": true, "comment": "主键ID"},
|
||||
{"name": "order_id", "type": "varchar(64)", "comment": "订单ID"},
|
||||
{"name": "contract_id", "type": "varchar(64)", "comment": "合同ID"},
|
||||
{"name": "customer_id", "type": "varchar(64)", "comment": "客户ID"},
|
||||
{"name": "receivable_amount", "type": "decimal(15,2)", "comment": "应收金额"},
|
||||
{"name": "received_amount", "type": "decimal(15,2)", "comment": "已收金额"},
|
||||
{"name": "due_date", "type": "date", "comment": "到期日期"},
|
||||
{"name": "status", "type": "varchar(32)", "comment": "状态"},
|
||||
{"name": "description", "type": "varchar(500)", "comment": "描述"},
|
||||
{"name": "org_id", "type": "varchar(64)", "comment": "组织ID"},
|
||||
{"name": "created_at", "type": "timestamp", "comment": "创建时间"},
|
||||
{"name": "updated_at", "type": "timestamp", "comment": "更新时间"}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_receivables_order_id",
|
||||
"idxtype": "index",
|
||||
"columns": ["order_id"]
|
||||
},
|
||||
{
|
||||
"name": "idx_receivables_contract_id",
|
||||
"idxtype": "index",
|
||||
"columns": ["contract_id"]
|
||||
},
|
||||
{
|
||||
"name": "idx_receivables_customer_id",
|
||||
"idxtype": "index",
|
||||
"columns": ["customer_id"]
|
||||
},
|
||||
{
|
||||
"name": "idx_receivables_status",
|
||||
"idxtype": "index",
|
||||
"columns": ["status"]
|
||||
},
|
||||
{
|
||||
"name": "idx_receivables_org_id",
|
||||
"idxtype": "index",
|
||||
"columns": ["org_id"]
|
||||
},
|
||||
{
|
||||
"name": "idx_receivables_due_date",
|
||||
"idxtype": "index",
|
||||
"columns": ["due_date"]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
{"name": "idx_receivables_customer", "fields": ["customer_id"]},
|
||||
{"name": "idx_receivables_contract", "fields": ["contract_id"]},
|
||||
{"name": "idx_receivables_status", "fields": ["status"]},
|
||||
{"name": "idx_receivables_due_date", "fields": ["due_date"]}
|
||||
]
|
||||
}
|
||||
82
mysql.ddl.sql
Normal file
82
mysql.ddl.sql
Normal file
@ -0,0 +1,82 @@
|
||||
-- Receivables table
|
||||
CREATE TABLE IF NOT EXISTS `receivables` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
|
||||
`order_id` VARCHAR(64) COMMENT '订单ID',
|
||||
`contract_id` VARCHAR(64) COMMENT '合同ID',
|
||||
`customer_id` VARCHAR(64) COMMENT '客户ID',
|
||||
`receivable_amount` DECIMAL(15,2) COMMENT '应收金额',
|
||||
`received_amount` DECIMAL(15,2) DEFAULT 0 COMMENT '已收金额',
|
||||
`due_date` DATE COMMENT '到期日期',
|
||||
`status` VARCHAR(32) DEFAULT 'pending' COMMENT '状态: pending/partial/completed',
|
||||
`description` VARCHAR(500) COMMENT '描述',
|
||||
`org_id` VARCHAR(64) COMMENT '组织ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_receivables_customer` (`customer_id`),
|
||||
KEY `idx_receivables_contract` (`contract_id`),
|
||||
KEY `idx_receivables_status` (`status`),
|
||||
KEY `idx_receivables_due_date` (`due_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应收管理';
|
||||
|
||||
-- Receipts table
|
||||
CREATE TABLE IF NOT EXISTS `receipts` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
|
||||
`customer_id` VARCHAR(64) COMMENT '客户ID',
|
||||
`total_amount` DECIMAL(15,2) COMMENT '收款总额',
|
||||
`receipt_date` DATE COMMENT '收款日期',
|
||||
`receipt_method` VARCHAR(32) COMMENT '收款方式',
|
||||
`status` VARCHAR(32) DEFAULT 'processed' COMMENT '状态',
|
||||
`org_id` VARCHAR(64) COMMENT '组织ID',
|
||||
`created_by` VARCHAR(64) COMMENT '创建人',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_receipts_customer` (`customer_id`),
|
||||
KEY `idx_receipts_date` (`receipt_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收款记录';
|
||||
|
||||
-- Payments table
|
||||
CREATE TABLE IF NOT EXISTS `payments` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
|
||||
`contract_id` VARCHAR(64) COMMENT '合同ID',
|
||||
`payment_amount` DECIMAL(15,2) COMMENT '支付金额',
|
||||
`payment_date` DATE COMMENT '支付日期',
|
||||
`payment_method` VARCHAR(32) COMMENT '支付方式',
|
||||
`status` VARCHAR(32) DEFAULT 'processed' COMMENT '状态',
|
||||
`org_id` VARCHAR(64) COMMENT '组织ID',
|
||||
`created_by` VARCHAR(64) COMMENT '创建人',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_payments_contract` (`contract_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支出记录';
|
||||
|
||||
-- Financial vouchers table
|
||||
CREATE TABLE IF NOT EXISTS `financial_vouchers` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
|
||||
`voucher_type` VARCHAR(32) COMMENT '凭证类型',
|
||||
`contract_id` VARCHAR(64) COMMENT '合同ID',
|
||||
`order_id` VARCHAR(64) COMMENT '订单ID',
|
||||
`amount` DECIMAL(15,2) COMMENT '金额',
|
||||
`voucher_date` DATE COMMENT '凭证日期',
|
||||
`description` TEXT COMMENT '描述',
|
||||
`reference_id` VARCHAR(64) COMMENT '关联ID',
|
||||
`org_id` VARCHAR(64) COMMENT '组织ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_vouchers_contract` (`contract_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='财务凭证';
|
||||
|
||||
-- Receipt allocations table
|
||||
CREATE TABLE IF NOT EXISTS `receipt_allocations` (
|
||||
`id` VARCHAR(64) NOT NULL COMMENT '主键ID',
|
||||
`receipt_id` VARCHAR(64) COMMENT '收款ID',
|
||||
`receivable_id` VARCHAR(64) COMMENT '应收ID',
|
||||
`allocated_amount` DECIMAL(15,2) COMMENT '分配金额',
|
||||
`org_id` VARCHAR(64) COMMENT '组织ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_allocations_receipt` (`receipt_id`),
|
||||
KEY `idx_allocations_receivable` (`receivable_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收款分配';
|
||||
18
wwwroot/api/debug_receivables.dspy
Normal file
18
wwwroot/api/debug_receivables.dspy
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
result = {'columns': []}
|
||||
try:
|
||||
dbname = get_module_dbname('financial_management')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
sql = "SELECT COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='crm_db' AND TABLE_NAME='receivables'"
|
||||
rows = await sor.sqlExe(sql, {'page': 1, 'rows': 50, 'sort': 'COLUMN_NAME'})
|
||||
if isinstance(rows, dict):
|
||||
rows = rows.get('rows', [])
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
result['columns'].append(d.get('column_name', '') + ' ' + d.get('column_type', ''))
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
30
wwwroot/api/receivables.dspy
Normal file
30
wwwroot/api/receivables.dspy
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Receivables list API"""
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('financial_management')
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': 'created_at desc'
|
||||
}
|
||||
sql = "SELECT id, order_id, contract_id, customer_id, receivable_amount, received_amount, due_date, status, description, org_id, created_at FROM receivables"
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
data = await sor.sqlExe(sql, ns)
|
||||
if isinstance(data, dict):
|
||||
result['total'] = data.get('total', 0)
|
||||
result['rows'] = [dict(r) for r in data.get('rows', [])]
|
||||
else:
|
||||
result['rows'] = [dict(r) for r in (data or [])]
|
||||
result['total'] = len(result['rows'])
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
38
wwwroot/api/receivables_create.dspy
Normal file
38
wwwroot/api/receivables_create.dspy
Normal file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Receivable create API"""
|
||||
import json, uuid, time
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
contract_id = params_kw.get('contract_id', '').strip()
|
||||
customer_id = params_kw.get('customer_id', '').strip()
|
||||
receivable_amount = params_kw.get('receivable_amount', '0').strip()
|
||||
due_date = params_kw.get('due_date', '').strip()
|
||||
|
||||
if not due_date:
|
||||
result['options'] = {'title': 'Error', 'message': '到期日不能为空', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('financial_management')
|
||||
new_id = str(uuid.uuid4()).replace('-', '')
|
||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe("""INSERT INTO receivables (id, order_id, contract_id, customer_id, receivable_amount, received_amount, due_date, status, description, org_id, created_at, updated_at)
|
||||
VALUES (${id}$, ${order_id}$, ${contract_id}$, ${customer_id}$, ${receivable_amount}$, ${received_amount}$, ${due_date}$, ${status}$, ${description}$, ${org_id}$, ${created_at}$, ${updated_at}$)""", {
|
||||
'id': new_id, 'order_id': params_kw.get('order_id', '').strip(),
|
||||
'contract_id': contract_id, 'customer_id': customer_id,
|
||||
'receivable_amount': float(receivable_amount) if receivable_amount else 0,
|
||||
'received_amount': 0, 'due_date': due_date,
|
||||
'status': params_kw.get('status', 'pending').strip(),
|
||||
'description': params_kw.get('description', '').strip(),
|
||||
'org_id': params_kw.get('org_id', '').strip(),
|
||||
'created_at': now, 'updated_at': now
|
||||
})
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '应收账款创建成功', 'type': 'success'}}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
25
wwwroot/api/receivables_delete.dspy
Normal file
25
wwwroot/api/receivables_delete.dspy
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Receivable delete API"""
|
||||
import json
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '').strip()
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': '记录ID不能为空', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('financial_management')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
existing = await sor.sqlExe("SELECT id FROM receivables WHERE id = ${id}$", {'id': record_id})
|
||||
if not existing:
|
||||
result['options'] = {'title': 'Error', 'message': '记录不存在', 'type': 'error'}
|
||||
else:
|
||||
await sor.sqlExe("DELETE FROM receivables WHERE id = ${id}$", {'id': record_id})
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '删除成功', 'type': 'success'}}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
23
wwwroot/api/receivables_list.dspy
Normal file
23
wwwroot/api/receivables_list.dspy
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""List receivables"""
|
||||
import json
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('financial_management')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
ns = {'page': 1, 'rows': 20, 'sort': 'id'}
|
||||
sql = 'SELECT id, order_id, contract_id, customer_id, receivable_amount, received_amount, due_date, status, description, org_id, created_at, updated_at FROM receivables'
|
||||
rows = await sor.sqlExe(sql, ns)
|
||||
if isinstance(rows, dict):
|
||||
result['rows'] = rows.get('rows', [])
|
||||
result['total'] = rows.get('total', 0)
|
||||
elif rows:
|
||||
result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows]
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
39
wwwroot/api/receivables_update.dspy
Normal file
39
wwwroot/api/receivables_update.dspy
Normal file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Receivable update API"""
|
||||
import json, time
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '').strip()
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': '记录ID不能为空', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('financial_management')
|
||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
fields = ['order_id', 'contract_id', 'customer_id', 'receivable_amount', 'received_amount', 'due_date', 'status', 'description', 'org_id']
|
||||
set_parts = []
|
||||
params = {'id': record_id}
|
||||
for f in fields:
|
||||
val = params_kw.get(f, None)
|
||||
if val is not None:
|
||||
set_parts.append(f"{f} = %({f})s")
|
||||
params[f] = val
|
||||
|
||||
set_parts.append("updated_at = ${updated_at}$")
|
||||
params['updated_at'] = now
|
||||
|
||||
if len(set_parts) <= 1:
|
||||
result['options'] = {'title': 'Error', 'message': '没有可更新的字段', 'type': 'error'}
|
||||
else:
|
||||
sql = f"UPDATE receivables SET {', '.join(set_parts)} WHERE id = ${id}$"
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe(sql, params)
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '更新成功', 'type': 'success'}}
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
14
wwwroot/api/test_env.dspy
Normal file
14
wwwroot/api/test_env.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
result = {'success': True}
|
||||
try:
|
||||
dbname = get_module_dbname('financial_management')
|
||||
result['dbname'] = dbname
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
sql = "SELECT count(*) rcnt FROM receivables"
|
||||
r = await sor.sqlExe(sql, {'page': 1, 'rows': 10, 'sort': 'id'})
|
||||
result['count'] = str(r)
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
30
wwwroot/financial_vouchers.ui
Normal file
30
wwwroot/financial_vouchers.ui
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px",
|
||||
"align": "center",
|
||||
"justify": "center"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Financial Vouchers",
|
||||
"fontSize": "24px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#1E40AF",
|
||||
"marginBottom": "10px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Feature under development",
|
||||
"fontSize": "16px",
|
||||
"color": "#6B7280"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
211
wwwroot/index.ui
Normal file
211
wwwroot/index.ui
Normal file
@ -0,0 +1,211 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px",
|
||||
"backgroundColor": "#F9FAFB"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Financial Management",
|
||||
"fontSize": "24px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#1E40AF",
|
||||
"marginBottom": "20px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "250px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderRadius": "8px",
|
||||
"padding": "20px",
|
||||
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.financial_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('receivables.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Receivables",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#1E40AF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Manage accounts receivable",
|
||||
"fontSize": "14px",
|
||||
"color": "#6B7280",
|
||||
"marginTop": "8px"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderRadius": "8px",
|
||||
"padding": "20px",
|
||||
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.financial_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('receipts.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Receipts",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#059669"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Manage incoming payments",
|
||||
"fontSize": "14px",
|
||||
"color": "#6B7280",
|
||||
"marginTop": "8px"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderRadius": "8px",
|
||||
"padding": "20px",
|
||||
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.financial_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('payments.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Payments",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#D97706"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Manage outgoing payments",
|
||||
"fontSize": "14px",
|
||||
"color": "#6B7280",
|
||||
"marginTop": "8px"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderRadius": "8px",
|
||||
"padding": "20px",
|
||||
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.financial_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('financial_vouchers.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Vouchers",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#7C3AED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Financial vouchers",
|
||||
"fontSize": "14px",
|
||||
"color": "#6B7280",
|
||||
"marginTop": "8px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "financial_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderRadius": "8px",
|
||||
"padding": "20px",
|
||||
"boxShadow": "0 1px 3px rgba(0,0,0,0.1)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
30
wwwroot/payments.ui
Normal file
30
wwwroot/payments.ui
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px",
|
||||
"align": "center",
|
||||
"justify": "center"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Payments Management",
|
||||
"fontSize": "24px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#1E40AF",
|
||||
"marginBottom": "10px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Feature under development",
|
||||
"fontSize": "16px",
|
||||
"color": "#6B7280"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
30
wwwroot/receipts.ui
Normal file
30
wwwroot/receipts.ui
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px",
|
||||
"align": "center",
|
||||
"justify": "center"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Receipts Management",
|
||||
"fontSize": "24px",
|
||||
"fontWeight": "bold",
|
||||
"color": "#1E40AF",
|
||||
"marginBottom": "10px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "Feature under development",
|
||||
"fontSize": "16px",
|
||||
"color": "#6B7280"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
45
wwwroot/receivable_edit.ui
Normal file
45
wwwroot/receivable_edit.ui
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"widgettype": "Page",
|
||||
"options": {
|
||||
"title": "编辑应收账款",
|
||||
"style": {"height": "100vh", "padding": "0"}
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "receivable_form",
|
||||
"options": {
|
||||
"submit_url": "{{entire_url('api/receivables_create.dspy')}}",
|
||||
"method": "POST",
|
||||
"layout": "vertical",
|
||||
"style": {"maxWidth": "600px"},
|
||||
"fields": [
|
||||
{"name": "order_id", "label": "订单号", "uitype": "text"},
|
||||
{"name": "contract_id", "label": "合同ID", "uitype": "text"},
|
||||
{"name": "customer_id", "label": "客户ID", "uitype": "text"},
|
||||
{"name": "receivable_amount", "label": "应收金额", "uitype": "number", "required": true},
|
||||
{"name": "due_date", "label": "到期日", "uitype": "date", "required": true},
|
||||
{"name": "status", "label": "状态", "uitype": "code", "data": [
|
||||
{"value": "pending", "text": "待收"},
|
||||
{"value": "partial", "text": "部分收款"},
|
||||
{"value": "received", "text": "已收款"},
|
||||
{"value": "overdue", "text": "逾期"}
|
||||
]},
|
||||
{"name": "received_amount", "label": "已收金额", "uitype": "number"},
|
||||
{"name": "description", "label": "说明", "uitype": "textarea"},
|
||||
{"name": "org_id", "label": "组织ID", "uitype": "text"}
|
||||
],
|
||||
"buttons": [
|
||||
{"type": "submit", "text": "保存", "variant": "primary"},
|
||||
{"type": "button", "text": "取消", "action": "navigate('main/financial_management/receivables.ui')"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
46
wwwroot/receivables.ui
Normal file
46
wwwroot/receivables.ui
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"widgettype": "Page",
|
||||
"options": {
|
||||
"title": "应收账款",
|
||||
"style": {"height": "100vh", "padding": "0"}
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {"style": {"marginBottom": "16px", "gap": "8px"}},
|
||||
"subwidgets": [
|
||||
{"widgettype": "TextField", "id": "search_keyword", "options": {"label": "搜索", "placeholder": "合同/客户", "style": {"flex": 1}}},
|
||||
{"widgettype": "Button", "id": "btn_search", "options": {"text": "搜索", "variant": "primary"}},
|
||||
{"widgettype": "Button", "id": "btn_add", "options": {"text": "新增", "variant": "primary", "action": "navigate('main/financial_management/receivable_edit.ui')"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "DataGrid",
|
||||
"id": "receivables_grid",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/receivables_list.dspy')}}",
|
||||
"style": {"flex": 1},
|
||||
"columns": [
|
||||
{"field": "id", "header": "编号", "width": 150},
|
||||
{"field": "contract_id", "header": "合同", "width": 140},
|
||||
{"field": "customer_id", "header": "客户", "width": 140},
|
||||
{"field": "receivable_amount", "header": "应收金额", "width": 120},
|
||||
{"field": "received_amount", "header": "已收金额", "width": 120},
|
||||
{"field": "due_date", "header": "到期日", "width": 110},
|
||||
{"field": "status", "header": "状态", "width": 90},
|
||||
{"field": "description", "header": "说明", "width": 200}
|
||||
],
|
||||
"toolbar": [
|
||||
{"type": "button", "text": "编辑", "icon": "edit", "action": "navigate('main/financial_management/receivable_edit.ui?id={% raw %}{{selectedRow.id}}{% endraw %}')"},
|
||||
{"type": "button", "text": "删除", "icon": "delete", "action": "doDelete('{% raw %}{{selectedRow.id}}{% endraw %}')"}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user