sync: local modifications to contract_management

- Updated core modules: __init__, ai_config, ai_core, attachment, contract, enhanced_contract
- Updated mysql.ddl.sql
- Updated UI files: contract_detail, contract_edit, contract_list
- Added new files: api/*.dspy, contract_list.dspy
This commit is contained in:
yumoqing 2026-04-28 18:49:46 +08:00
parent 3772f0f6fb
commit a72637b2a3
16 changed files with 490 additions and 467 deletions

View File

@ -1,15 +1,10 @@
""" """
合同管理模块 - 增强版 合同管理模块
支持从商机创建合同自动生成里程碑和订单AI智能审核等功能 支持合同全生命周期管理
""" """
from .enhanced_contract_core import ( from .contract_core import (
create_contract, create_contract,
create_contract_from_opportunity,
create_contract_version,
create_manual_order,
update_contract_milestone_status,
ai_contract_analysis,
update_contract, update_contract,
delete_contract, delete_contract,
get_contract_by_id, get_contract_by_id,
@ -17,18 +12,13 @@ from .enhanced_contract_core import (
) )
# 版本信息 # 版本信息
__version__ = "2.0.0" __version__ = "1.0.0"
__author__ = "Hermes AI Agent" __author__ = "Hermes AI Agent"
__description__ = "Enhanced Contract Management Module with Opportunity Integration and AI Analysis" __description__ = "Contract Management Module"
# 导出所有公共接口 # 导出所有公共接口
__all__ = [ __all__ = [
'create_contract', 'create_contract',
'create_contract_from_opportunity',
'create_contract_version',
'create_manual_order',
'update_contract_milestone_status',
'ai_contract_analysis',
'update_contract', 'update_contract',
'delete_contract', 'delete_contract',
'get_contract_by_id', 'get_contract_by_id',

View File

@ -1,6 +1,6 @@
import os import os
import json import json
from sqlor.dbp import getDBP from sqlor.dbpools import DBPools
async def save_ai_config(request_data: dict, org_id: str) -> dict: async def save_ai_config(request_data: dict, org_id: str) -> dict:
"""保存AI配置""" """保存AI配置"""
@ -8,7 +8,7 @@ async def save_ai_config(request_data: dict, org_id: str) -> dict:
# 检查是否已存在配置 # 检查是否已存在配置
check_sql = "SELECT id FROM contract_ai_config WHERE org_id = %(org_id)s" check_sql = "SELECT id FROM contract_ai_config WHERE org_id = %(org_id)s"
check_result = await dbp.doQuery(check_sql, {'org_id': org_id}) check_result = await sor.doQuery(check_sql, {'org_id': org_id})
if check_result: if check_result:
# 更新现有配置 # 更新现有配置
@ -26,7 +26,7 @@ async def save_ai_config(request_data: dict, org_id: str) -> dict:
request_data['id'] = str(uuid.uuid4()).replace('-', '') request_data['id'] = str(uuid.uuid4()).replace('-', '')
request_data['org_id'] = org_id request_data['org_id'] = org_id
await dbp.doTransaction([{'sql': sql, 'params': request_data}]) await sor.doTransaction([{'sql': sql, 'params': request_data}])
return {'success': True, 'message': 'AI配置保存成功'} return {'success': True, 'message': 'AI配置保存成功'}
# 在 init.py 中添加这个函数的暴露 # 在 init.py 中添加这个函数的暴露

View File

@ -1,7 +1,7 @@
import json import json
import aiohttp import aiohttp
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from appPublic.jsonconfig import getConfig from appPublic.Config import getConfig
class AIContractService: class AIContractService:
def __init__(self): def __init__(self):

View File

@ -2,14 +2,14 @@ import os
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import List, Dict, Optional from typing import List, Dict, Optional
from appPublic.jsonconfig import getConfig from appPublic.Config import getConfig
from appPublic.worker import Worker # Worker removed - not available
from sqlor.dbp import getDBP from sqlor.dbpools import DBPools
class ContractAttachmentManager: class ContractAttachmentManager:
def __init__(self): def __init__(self):
self.config = getConfig() self.config = getConfig()
self.worker = Worker() pass # Worker removed
self.upload_dir = self.config.get('contract_upload_dir', '/var/uploads/contracts') self.upload_dir = self.config.get('contract_upload_dir', '/var/uploads/contracts')
async def get_db_connection(self, org_id: str): async def get_db_connection(self, org_id: str):
@ -49,7 +49,7 @@ class ContractAttachmentManager:
SELECT MAX(version) as max_version FROM contract_attachment SELECT MAX(version) as max_version FROM contract_attachment
WHERE contract_id = %(contract_id)s AND file_name = %(file_name)s AND org_id = %(org_id)s WHERE contract_id = %(contract_id)s AND file_name = %(file_name)s AND org_id = %(org_id)s
""" """
check_result = await dbp.doQuery(check_sql, { check_result = await sor.doQuery(check_sql, {
'contract_id': contract_id, 'contract_id': contract_id,
'file_name': file_name, 'file_name': file_name,
'org_id': org_id 'org_id': org_id
@ -81,7 +81,7 @@ class ContractAttachmentManager:
'org_id': org_id 'org_id': org_id
} }
await dbp.doTransaction([{'sql': sql, 'params': params}]) await sor.doTransaction([{'sql': sql, 'params': params}])
return attachment_id return attachment_id
async def get_contract_attachments(self, contract_id: str, org_id: str) -> List[Dict]: async def get_contract_attachments(self, contract_id: str, org_id: str) -> List[Dict]:
@ -92,7 +92,7 @@ class ContractAttachmentManager:
WHERE contract_id = %(contract_id)s AND org_id = %(org_id)s WHERE contract_id = %(contract_id)s AND org_id = %(org_id)s
ORDER BY version DESC, created_at DESC ORDER BY version DESC, created_at DESC
""" """
result = await dbp.doQuery(sql, {'contract_id': contract_id, 'org_id': org_id}) result = await sor.doQuery(sql, {'contract_id': contract_id, 'org_id': org_id})
return result return result
# 全局实例 # 全局实例

View File

@ -3,14 +3,12 @@ import json
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
from appPublic.jsonconfig import getConfig from appPublic.Config import getConfig
from appPublic.worker import Worker from sqlor.dbpools import DBPools
from sqlor.dbp import getDBP
class ContractManager: class ContractManager:
def __init__(self): def __init__(self):
self.config = getConfig() self.config = getConfig()
self.worker = Worker()
async def get_db_connection(self, org_id: str): async def get_db_connection(self, org_id: str):
"""获取数据库连接""" """获取数据库连接"""
@ -52,7 +50,7 @@ class ContractManager:
'org_id': org_id 'org_id': org_id
} }
await dbp.doTransaction([{'sql': sql, 'params': params}]) await sor.doTransaction([{'sql': sql, 'params': params}])
return contract_id return contract_id
async def update_contract(self, contract_id: str, contract_data: Dict, org_id: str) -> bool: async def update_contract(self, contract_id: str, contract_data: Dict, org_id: str) -> bool:
@ -72,21 +70,21 @@ class ContractManager:
update_fields.append("updated_at = NOW()") update_fields.append("updated_at = NOW()")
sql = f"UPDATE contract SET {', '.join(update_fields)} WHERE id = %(id)s AND org_id = %(org_id)s" sql = f"UPDATE contract SET {', '.join(update_fields)} WHERE id = %(id)s AND org_id = %(org_id)s"
result = await dbp.doTransaction([{'sql': sql, 'params': params}]) result = await sor.doTransaction([{'sql': sql, 'params': params}])
return result.rowcount > 0 return result.rowcount > 0
async def delete_contract(self, contract_id: str, org_id: str) -> bool: async def delete_contract(self, contract_id: str, org_id: str) -> bool:
"""删除合同(软删除)""" """删除合同(软删除)"""
dbp = await self.get_db_connection(org_id) dbp = await self.get_db_connection(org_id)
sql = "UPDATE contract SET status = 'deleted', updated_at = NOW() WHERE id = %(id)s AND org_id = %(org_id)s" sql = "UPDATE contract SET status = 'deleted', updated_at = NOW() WHERE id = %(id)s AND org_id = %(org_id)s"
result = await dbp.doTransaction([{'sql': sql, 'params': {'id': contract_id, 'org_id': org_id}}]) result = await sor.doTransaction([{'sql': sql, 'params': {'id': contract_id, 'org_id': org_id}}])
return result.rowcount > 0 return result.rowcount > 0
async def get_contract_by_id(self, contract_id: str, org_id: str) -> Optional[Dict]: async def get_contract_by_id(self, contract_id: str, org_id: str) -> Optional[Dict]:
"""根据ID获取合同""" """根据ID获取合同"""
dbp = await self.get_db_connection(org_id) dbp = await self.get_db_connection(org_id)
sql = "SELECT * FROM contract WHERE id = %(id)s AND org_id = %(org_id)s AND status != 'deleted'" sql = "SELECT * FROM contract WHERE id = %(id)s AND org_id = %(org_id)s AND status != 'deleted'"
result = await dbp.doQuery(sql, {'id': contract_id, 'org_id': org_id}) result = await sor.doQuery(sql, {'id': contract_id, 'org_id': org_id})
return result[0] if result else None return result[0] if result else None
async def list_contracts(self, org_id: str, filters: Optional[Dict] = None, async def list_contracts(self, org_id: str, filters: Optional[Dict] = None,
@ -125,7 +123,7 @@ class ContractManager:
# 获取总数 # 获取总数
count_sql = f"SELECT COUNT(*) as total FROM contract WHERE {where_sql}" count_sql = f"SELECT COUNT(*) as total FROM contract WHERE {where_sql}"
count_result = await dbp.doQuery(count_sql, params) count_result = await sor.doQuery(count_sql, params)
total = count_result[0]['total'] if count_result else 0 total = count_result[0]['total'] if count_result else 0
# 获取分页数据 # 获取分页数据
@ -139,7 +137,7 @@ class ContractManager:
params['limit'] = page_size params['limit'] = page_size
params['offset'] = offset params['offset'] = offset
data_result = await dbp.doQuery(data_sql, params) data_result = await sor.doQuery(data_sql, params)
return data_result, total return data_result, total
# 全局实例 # 全局实例

View File

@ -3,15 +3,14 @@ import json
import uuid import uuid
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from typing import List, Dict, Optional, Tuple from typing import List, Dict, Optional, Tuple
from appPublic.jsonconfig import getConfig from appPublic.Config import getConfig
from appPublic.worker import Worker from appPublic.worker import awaitify
from sqlor.dbp import getDBP from sqlor.dbpools import DBPools
class EnhancedContractManager: class EnhancedContractManager:
def __init__(self): def __init__(self):
self.config = getConfig() self.config = getConfig()
self.worker = Worker()
async def get_db_connection(self, org_id: str): async def get_db_connection(self, org_id: str):
"""获取数据库连接""" """获取数据库连接"""
@ -22,7 +21,7 @@ class EnhancedContractManager:
"""从商机创建合同""" """从商机创建合同"""
# 获取商机信息 # 获取商机信息
opp_dbp = await getDBP(org_id) opp_dbp = await getDBP(org_id)
opportunity = await opp_dbp.select_one("opportunities", {"id": opportunity_id}) opportunity = await opp_sor.select_one("opportunities", {"id": opportunity_id})
if not opportunity: if not opportunity:
raise ValueError("商机不存在") raise ValueError("商机不存在")
@ -90,7 +89,7 @@ class EnhancedContractManager:
'tax_rate': contract_data.get('tax_rate', '0.1300') 'tax_rate': contract_data.get('tax_rate', '0.1300')
} }
await dbp.doTransaction([{'sql': sql, 'params': params}]) await sor.doTransaction([{'sql': sql, 'params': params}])
# 创建初始版本 # 创建初始版本
await self.create_contract_version(contract_id, contract_data.get('content', ''), user_id, org_id, 'v1.0', '初始版本') await self.create_contract_version(contract_id, contract_data.get('content', ''), user_id, org_id, 'v1.0', '初始版本')
@ -104,7 +103,7 @@ class EnhancedContractManager:
dbp = await self.get_db_connection(org_id) dbp = await self.get_db_connection(org_id)
# 将之前的当前版本标记为非当前 # 将之前的当前版本标记为非当前
await dbp.update( await sor.update(
"contract_versions", "contract_versions",
{"is_current": "0"}, {"is_current": "0"},
{"contract_id": contract_id, "is_current": "1"} {"contract_id": contract_id, "is_current": "1"}
@ -123,7 +122,7 @@ class EnhancedContractManager:
"is_current": "1" "is_current": "1"
} }
await dbp.insert("contract_versions", version_data) await sor.insert("contract_versions", version_data)
return version_id return version_id
async def generate_milestones_from_payment_terms(self, contract_id: str, payment_terms: str, org_id: str): async def generate_milestones_from_payment_terms(self, contract_id: str, payment_terms: str, org_id: str):
@ -173,7 +172,7 @@ class EnhancedContractManager:
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
} }
dbp = await self.get_db_connection(org_id) dbp = await self.get_db_connection(org_id)
await dbp.insert("contract_milestones", milestone_data) await sor.insert("contract_milestones", milestone_data)
async def generate_orders_from_payment_terms(self, contract_id: str, contract_data: Dict, org_id: str): async def generate_orders_from_payment_terms(self, contract_id: str, contract_data: Dict, org_id: str):
"""根据付款条款自动生成订单""" """根据付款条款自动生成订单"""
@ -221,7 +220,7 @@ class EnhancedContractManager:
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
} }
dbp = await self.get_db_connection(org_id) dbp = await self.get_db_connection(org_id)
await dbp.insert("orders", order_data) await sor.insert("orders", order_data)
async def create_manual_order(self, contract_id: str, order_data: Dict, user_id: str, org_id: str) -> str: async def create_manual_order(self, contract_id: str, order_data: Dict, user_id: str, org_id: str) -> str:
"""手动创建订单""" """手动创建订单"""
@ -232,7 +231,7 @@ class EnhancedContractManager:
# 计算已收款金额 # 计算已收款金额
dbp = await self.get_db_connection(org_id) dbp = await self.get_db_connection(org_id)
paid_orders = await dbp.query( paid_orders = await sor.query(
"SELECT SUM(amount) as total_paid FROM orders WHERE contract_id = %(contract_id)s AND status != 'cancelled'", "SELECT SUM(amount) as total_paid FROM orders WHERE contract_id = %(contract_id)s AND status != 'cancelled'",
{"contract_id": contract_id} {"contract_id": contract_id}
) )
@ -264,7 +263,7 @@ class EnhancedContractManager:
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
} }
await dbp.insert("orders", final_order_data) await sor.insert("orders", final_order_data)
return order_id return order_id
async def update_contract_milestone_status(self, milestone_id: str, status: str, actual_date: str = None, org_id: str = None): async def update_contract_milestone_status(self, milestone_id: str, status: str, actual_date: str = None, org_id: str = None):
@ -277,7 +276,7 @@ class EnhancedContractManager:
if actual_date: if actual_date:
update_data["actual_date"] = actual_date update_data["actual_date"] = actual_date
await dbp.update("contract_milestones", update_data, {"id": milestone_id}) await sor.update("contract_milestones", update_data, {"id": milestone_id})
# 如果里程碑完成,更新关联的合同履约进度 # 如果里程碑完成,更新关联的合同履约进度
if status == "completed": if status == "completed":
@ -286,13 +285,13 @@ class EnhancedContractManager:
async def update_contract_fulfillment_progress(self, milestone_id: str, org_id: str): async def update_contract_fulfillment_progress(self, milestone_id: str, org_id: str):
"""更新合同履约进度""" """更新合同履约进度"""
dbp = await self.get_db_connection(org_id) dbp = await self.get_db_connection(org_id)
milestone = await dbp.select_one("contract_milestones", {"id": milestone_id}) milestone = await sor.select_one("contract_milestones", {"id": milestone_id})
if not milestone: if not milestone:
return return
# 计算履约进度(简化逻辑) # 计算履约进度(简化逻辑)
contract_id = milestone["contract_id"] contract_id = milestone["contract_id"]
all_milestones = await dbp.query( all_milestones = await sor.query(
"SELECT COUNT(*) as total, SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed FROM contract_milestones WHERE contract_id = %(contract_id)s", "SELECT COUNT(*) as total, SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed FROM contract_milestones WHERE contract_id = %(contract_id)s",
{"contract_id": contract_id} {"contract_id": contract_id}
) )
@ -303,12 +302,12 @@ class EnhancedContractManager:
progress = (completed / total * 100) if total > 0 else 0 progress = (completed / total * 100) if total > 0 else 0
# 更新商机阶段(如果存在关联商机) # 更新商机阶段(如果存在关联商机)
contract = await dbp.select_one("contract", {"id": contract_id}) contract = await sor.select_one("contract", {"id": contract_id})
if contract and contract.get("opportunity_id"): if contract and contract.get("opportunity_id"):
# 这里应该调用商机管理模块的API来更新阶段 # 这里应该调用商机管理模块的API来更新阶段
# 模拟实现当进度达到100%时,将商机状态更新为成交 # 模拟实现当进度达到100%时,将商机状态更新为成交
if progress >= 100: if progress >= 100:
await dbp.update( await sor.update(
"opportunities", "opportunities",
{"status": "won", "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}, {"status": "won", "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
{"id": contract["opportunity_id"]} {"id": contract["opportunity_id"]}
@ -338,7 +337,7 @@ class EnhancedContractManager:
ORDER BY cm.planned_date ASC ORDER BY cm.planned_date ASC
""" """
overdue_milestones = await dbp.doQuery(sql, {"org_id": org_id}) overdue_milestones = await sor.doQuery(sql, {"org_id": org_id})
return overdue_milestones return overdue_milestones
async def send_milestone_reminders(self, org_id: str) -> int: async def send_milestone_reminders(self, org_id: str) -> int:
@ -498,202 +497,3 @@ class EnhancedContractManager:
for pattern in patterns: for pattern in patterns:
match = re.search(pattern, content) match = re.search(pattern, content)
if match:
return f"{match.group(1)}%"
return "未明确"
def _extract_key_dates(self, content: str) -> Dict:
"""提取关键日期"""
import re
from datetime import datetime
key_dates = {}
# 提取合同开始日期
start_patterns = [r'合同生效日期[:]?\s*(\d{4}[-年]\d{1,2}[-月]\d{1,2})', r'开始日期[:]?\s*(\d{4}[-年]\d{1,2}[-月]\d{1,2})']
for pattern in start_patterns:
match = re.search(pattern, content)
if match:
date_str = match.group(1).replace('', '-').replace('', '-').replace('', '')
try:
key_dates["start_date"] = {"type": "start_date", "date": date_str, "description": "合同开始日期"}
except:
pass
# 提取合同结束日期
end_patterns = [r'合同终止日期[:]?\s*(\d{4}[-年]\d{1,2}[-月]\d{1,2})', r'结束日期[:]?\s*(\d{4}[-年]\d{1,2}[-月]\d{1,2})']
for pattern in end_patterns:
match = re.search(pattern, content)
if match:
date_str = match.group(1).replace('', '-').replace('', '-').replace('', '')
try:
key_dates["end_date"] = {"type": "end_date", "date": date_str, "description": "合同结束日期"}
except:
pass
# 提取签署日期
sign_patterns = [r'签署日期[:]?\s*(\d{4}[-年]\d{1,2}[-月]\d{1,2})', r'签订日期[:]?\s*(\d{4}[-年]\d{1,2}[-月]\d{1,2})']
for pattern in sign_patterns:
match = re.search(pattern, content)
if match:
date_str = match.group(1).replace('', '-').replace('', '-').replace('', '')
try:
key_dates["sign_date"] = {"type": "sign_date", "date": date_str, "description": "合同签署日期"}
except:
pass
return key_dates
def _is_long_credit_period(self, credit_period: str) -> bool:
"""判断是否为超长账期(>90天"""
import re
if credit_period == "未明确":
return False
match = re.search(r'(\d+)', credit_period)
if match:
days = int(match.group(1))
return days > 90
return False
def _is_abnormal_penalty(self, penalty_clause: str) -> bool:
"""判断是否为异常违约金条款"""
import re
if penalty_clause == "未明确":
return False
match = re.search(r'(\d+)', penalty_clause)
if match:
percentage = int(match.group(1))
return percentage > 30 # 超过30%视为异常
return False
# 保留原有的合同管理方法
async def update_contract(self, contract_id: str, contract_data: Dict, org_id: str) -> bool:
"""更新合同"""
dbp = await self.get_db_connection(org_id)
# 构建更新SQL
update_fields = []
params = {'id': contract_id, 'org_id': org_id}
for field, value in contract_data.items():
if field in ['contract_number', 'title', 'party_a', 'party_b', 'contract_type',
'status', 'amount', 'start_date', 'end_date', 'sign_date', 'description',
'payment_terms', 'credit_period', 'penalty_clause', 'tax_rate']:
update_fields.append(f"{field} = %({field})s")
params[field] = value
update_fields.append("updated_at = NOW()")
sql = f"UPDATE contract SET {', '.join(update_fields)} WHERE id = %(id)s AND org_id = %(org_id)s"
result = await dbp.doTransaction([{'sql': sql, 'params': params}])
return result.rowcount > 0
async def delete_contract(self, contract_id: str, org_id: str) -> bool:
"""删除合同(软删除)"""
dbp = await self.get_db_connection(org_id)
sql = "UPDATE contract SET status = 'deleted', updated_at = NOW() WHERE id = %(id)s AND org_id = %(org_id)s"
result = await dbp.doTransaction([{'sql': sql, 'params': {'id': contract_id, 'org_id': org_id}}])
return result.rowcount > 0
async def get_contract_by_id(self, contract_id: str, org_id: str) -> Optional[Dict]:
"""根据ID获取合同"""
dbp = await self.get_db_connection(org_id)
sql = "SELECT * FROM contract WHERE id = %(id)s AND org_id = %(org_id)s AND status != 'deleted'"
result = await dbp.doQuery(sql, {'id': contract_id, 'org_id': org_id})
return result[0] if result else None
async def list_contracts(self, org_id: str, filters: Optional[Dict] = None,
page: int = 1, page_size: int = 20) -> Tuple[List[Dict], int]:
"""列出合同"""
dbp = await self.get_db_connection(org_id)
# 构建查询条件
where_clauses = ["org_id = %(org_id)s", "status != 'deleted'"]
params = {'org_id': org_id}
if filters:
if filters.get('contract_number'):
where_clauses.append("contract_number LIKE %(contract_number)s")
params['contract_number'] = f"%{filters['contract_number']}%"
if filters.get('title'):
where_clauses.append("title LIKE %(title)s")
params['title'] = f"%{filters['title']}%"
if filters.get('party_b'):
where_clauses.append("party_b LIKE %(party_b)s")
params['party_b'] = f"%{filters['party_b']}%"
if filters.get('status'):
where_clauses.append("status = %(status)s")
params['status'] = filters['status']
if filters.get('contract_type'):
where_clauses.append("contract_type = %(contract_type)s")
params['contract_type'] = filters['contract_type']
if filters.get('start_date_from'):
where_clauses.append("start_date >= %(start_date_from)s")
params['start_date_from'] = filters['start_date_from']
if filters.get('end_date_to'):
where_clauses.append("end_date <= %(end_date_to)s")
params['end_date_to'] = filters['end_date_to']
where_sql = " AND ".join(where_clauses)
# 获取总数
count_sql = f"SELECT COUNT(*) as total FROM contract WHERE {where_sql}"
count_result = await dbp.doQuery(count_sql, params)
total = count_result[0]['total'] if count_result else 0
# 获取分页数据
offset = (page - 1) * page_size
data_sql = f"""
SELECT * FROM contract
WHERE {where_sql}
ORDER BY created_at DESC
LIMIT %(limit)s OFFSET %(offset)s
"""
params['limit'] = page_size
params['offset'] = offset
data_result = await dbp.doQuery(data_sql, params)
return data_result, total
# 全局实例
enhanced_contract_manager = EnhancedContractManager()
# 导出函数
async def create_contract_from_opportunity(opportunity_id: str, contract_data: Dict, user_id: str, org_id: str) -> str:
return await enhanced_contract_manager.create_contract_from_opportunity(opportunity_id, contract_data, user_id, org_id)
async def create_contract(contract_data: Dict, user_id: str, org_id: str) -> str:
return await enhanced_contract_manager.create_contract(contract_data, user_id, org_id)
async def create_contract_version(contract_id: str, content: str, user_id: str, org_id: str,
version_number: str, modified_reason: str = '') -> str:
return await enhanced_contract_manager.create_contract_version(contract_id, content, user_id, org_id, version_number, modified_reason)
async def create_manual_order(contract_id: str, order_data: Dict, user_id: str, org_id: str) -> str:
return await enhanced_contract_manager.create_manual_order(contract_id, order_data, user_id, org_id)
async def update_contract_milestone_status(milestone_id: str, status: str, actual_date: str = None, org_id: str = None):
return await enhanced_contract_manager.update_contract_milestone_status(milestone_id, status, actual_date, org_id)
async def ai_contract_analysis(contract_content: str, org_id: str) -> Dict:
return await enhanced_contract_manager.ai_contract_analysis(contract_content, org_id)
# 保留原有接口
async def update_contract(contract_id: str, contract_data: Dict, org_id: str) -> bool:
return await enhanced_contract_manager.update_contract(contract_id, contract_data, org_id)
async def delete_contract(contract_id: str, org_id: str) -> bool:
return await enhanced_contract_manager.delete_contract(contract_id, org_id)
async def get_contract_by_id(contract_id: str, org_id: str) -> Optional[Dict]:
return await enhanced_contract_manager.get_contract_by_id(contract_id, org_id)
async def list_contracts(org_id: str, filters: Optional[Dict] = None,
page: int = 1, page_size: int = 20) -> Tuple[List[Dict], int]:
return await enhanced_contract_manager.list_contracts(org_id, filters, page, page_size)

View File

@ -0,0 +1,151 @@
-- Table from contract.json
CREATE TABLE IF NOT EXISTS `contract` (
`id` VARCHAR(32) NOT NULL COMMENT '合同ID主键',
`contract_number` VARCHAR(50) NOT NULL COMMENT '合同编号,唯一标识',
`title` VARCHAR(200) NOT NULL COMMENT '合同标题',
`party_a` VARCHAR(100) NOT NULL COMMENT '甲方(我方)',
`party_b` VARCHAR(100) NOT NULL COMMENT '乙方(对方)',
`contract_type` VARCHAR(32) NOT NULL COMMENT '合同类型引用appcodes表的id',
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT '合同状态draft-草稿, active-生效, expired-过期, terminated-终止',
`amount` DECIMAL(15,2) COMMENT '合同金额',
`start_date` DATE NOT NULL COMMENT '合同开始日期',
`end_date` DATE NOT NULL COMMENT '合同结束日期',
`sign_date` DATE COMMENT '签署日期',
`owner_id` VARCHAR(32) NOT NULL COMMENT '合同负责人引用users表的id',
`org_id` VARCHAR(32) NOT NULL COMMENT '所属组织引用organization表的id',
`ai_compliance_result` TEXT COMMENT 'AI合规检查结果',
`ai_key_dates` TEXT COMMENT 'AI提取的关键时点JSON数据',
`payment_terms` TEXT COMMENT '付款节点规则,如\'30%+50%+20%\'',
`credit_period` INT COMMENT '账期天数',
`penalty_clause` TEXT COMMENT '违约金相关条款',
`opportunity_id` VARCHAR(32) COMMENT '关联的商机ID',
`customer_id` VARCHAR(32) COMMENT '关联的客户ID',
`tax_rate` DECIMAL(5,4) DEFAULT '0.1300' COMMENT '税率默认13%',
`created_at` TIMESTAMP NOT NULL COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同表';
CREATE UNIQUE INDEX `idx_contract_number` ON `contract` (`contract_number`);
CREATE INDEX `idx_contract_org` ON `contract` (`org_id`);
CREATE INDEX `idx_contract_status` ON `contract` (`status`);
-- Table from contract_ai_config.json
CREATE TABLE IF NOT EXISTS `contract_ai_config` (
`id` VARCHAR(32) NOT NULL COMMENT 'AI配置ID',
`ai_service_url` VARCHAR(500) NOT NULL COMMENT 'AI服务URL地址',
`api_key` VARCHAR(255) NOT NULL COMMENT 'API密钥',
`org_id` VARCHAR(32) NOT NULL COMMENT '所属组织',
`created_at` TIMESTAMP NOT NULL COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI配置表';
CREATE UNIQUE INDEX `idx_ai_config_org` ON `contract_ai_config` (`org_id`);
-- Table from contract_attachment.json
CREATE TABLE IF NOT EXISTS `contract_attachment` (
`id` VARCHAR(32) NOT NULL COMMENT '附件ID主键',
`contract_id` VARCHAR(32) NOT NULL COMMENT '关联的合同ID引用contract表的id',
`file_name` VARCHAR(255) NOT NULL COMMENT '文件名',
`file_path` VARCHAR(500) NOT NULL COMMENT '文件存储路径',
`file_size` INT NOT NULL COMMENT '文件大小(字节)',
`file_type` VARCHAR(50) NOT NULL COMMENT '文件类型MIME类型',
`version` INT NOT NULL DEFAULT '1' COMMENT '文件版本号',
`description` VARCHAR(200) COMMENT '附件描述',
`uploaded_by` VARCHAR(32) NOT NULL COMMENT '上传人引用users表的id',
`org_id` VARCHAR(32) NOT NULL COMMENT '所属组织',
`created_at` TIMESTAMP NOT NULL COMMENT '上传时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同附件表';
CREATE INDEX `idx_attachment_contract` ON `contract_attachment` (`contract_id`);
CREATE INDEX `idx_attachment_org` ON `contract_attachment` (`org_id`);
CREATE INDEX `idx_attachment_version` ON `contract_attachment` (`file_name`, `version`);
-- Table from contract_milestones.json
CREATE TABLE IF NOT EXISTS `contract_milestones` (
`id` VARCHAR(32) NOT NULL COMMENT '主键 - UUID格式',
`contract_id` VARCHAR(32) NOT NULL COMMENT '关联的合同ID',
`milestone_name` VARCHAR(100) NOT NULL COMMENT '里程碑名称,如\'\'、\'\'、\'\'',
`milestone_type` VARCHAR(20) NOT NULL COMMENT '里程碑类型payment=付款, delivery=交付, acceptance=验收',
`planned_date` DATE NOT NULL COMMENT '计划完成日期',
`actual_date` DATE COMMENT '实际完成日期',
`amount` DECIMAL(15,2) COMMENT '该里程碑关联的金额',
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态pending=待处理, completed=已完成, overdue=已逾期',
`description` TEXT COMMENT '里程碑详细描述',
`created_at` TIMESTAMP NOT NULL COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL COMMENT '最后更新时间',
`reminder_sent` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '逾期提醒是否已发送1=是, 0=否',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同里程碑管理表';
CREATE INDEX `idx_milestones_contract` ON `contract_milestones` (`contract_id`);
CREATE INDEX `idx_milestones_status` ON `contract_milestones` (`status`);
CREATE INDEX `idx_milestones_planned` ON `contract_milestones` (`planned_date`);
-- Table from contract_versions.json
CREATE TABLE IF NOT EXISTS `contract_versions` (
`id` VARCHAR(32) NOT NULL COMMENT '主键 - UUID格式',
`contract_id` VARCHAR(32) NOT NULL COMMENT '关联的合同ID',
`version_number` VARCHAR(20) NOT NULL COMMENT '版本号如v1.0, v1.1等',
`content` TEXT NOT NULL COMMENT '合同完整内容或差异内容',
`diff_content` TEXT COMMENT '与上一版本的差异内容HTML格式',
`modified_by` VARCHAR(32) NOT NULL COMMENT '修改人用户ID',
`modified_reason` TEXT COMMENT '版本修改原因',
`created_at` TIMESTAMP NOT NULL COMMENT '版本创建时间',
`is_current` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '是否为当前生效版本1=是, 0=否',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同版本管理表';
CREATE INDEX `idx_contract_versions_contract` ON `contract_versions` (`contract_id`);
CREATE UNIQUE INDEX `idx_contract_versions_version` ON `contract_versions` (`contract_id`, `version_number`);
CREATE INDEX `idx_contract_versions_current` ON `contract_versions` (`contract_id`, `is_current`);
-- Table from order_payments.json
CREATE TABLE IF NOT EXISTS `order_payments` (
`id` VARCHAR(32) NOT NULL COMMENT '主键 - UUID格式',
`order_id` VARCHAR(32) NOT NULL COMMENT '关联的订单ID',
`payment_amount` DECIMAL(15,2) NOT NULL COMMENT '本次付款金额',
`payment_date` DATE NOT NULL COMMENT '付款日期',
`payment_method` VARCHAR(50) COMMENT '付款方式',
`payment_reference` VARCHAR(100) COMMENT '付款凭证号或参考号',
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态pending=待确认, confirmed=已确认, rejected=已拒绝',
`notes` TEXT COMMENT '付款备注',
`created_at` TIMESTAMP NOT NULL COMMENT '创建时间',
`confirmed_at` TIMESTAMP COMMENT '付款确认时间',
`confirmed_by` VARCHAR(32) COMMENT '付款确认人ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单付款记录表';
CREATE INDEX `idx_payments_order` ON `order_payments` (`order_id`);
CREATE INDEX `idx_payments_status` ON `order_payments` (`status`);
CREATE INDEX `idx_payments_date` ON `order_payments` (`payment_date`);
-- Table from orders.json
CREATE TABLE IF NOT EXISTS `orders` (
`id` VARCHAR(32) NOT NULL COMMENT '主键 - UUID格式',
`order_number` VARCHAR(50) NOT NULL COMMENT '订单编号,唯一标识',
`contract_id` VARCHAR(32) NOT NULL COMMENT '关联的合同ID',
`customer_id` VARCHAR(32) NOT NULL COMMENT '客户ID',
`order_type` VARCHAR(20) NOT NULL COMMENT '订单类型advance=预付款, progress=进度款, final=尾款, other=其他',
`delivery_batch` VARCHAR(100) COMMENT '交付批次,如\'Q3季度服务交付\'',
`acceptance_deadline` DATE COMMENT '验收截止日期',
`amount` DECIMAL(15,2) NOT NULL COMMENT '该订单的金额',
`paid_amount` DECIMAL(15,2) NOT NULL DEFAULT '0.00' COMMENT '已支付金额',
`tax_rate` DECIMAL(5,4) NOT NULL DEFAULT '0.1300' COMMENT '税率',
`credit_period` INT COMMENT '账期天数要求',
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态active=活跃, completed=完成, cancelled=取消',
`description` TEXT COMMENT '订单详细描述',
`created_at` TIMESTAMP NOT NULL COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL COMMENT '最后更新时间',
`owner_id` VARCHAR(32) NOT NULL COMMENT '订单负责人ID',
`org_id` VARCHAR(32) NOT NULL COMMENT '所属组织ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
CREATE UNIQUE INDEX `idx_orders_number` ON `orders` (`order_number`);
CREATE INDEX `idx_orders_contract` ON `orders` (`contract_id`);
CREATE INDEX `idx_orders_customer` ON `orders` (`customer_id`);
CREATE INDEX `idx_orders_status` ON `orders` (`status`);
CREATE INDEX `idx_orders_owner` ON `orders` (`owner_id`);

View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
result = {'keys': [], 'rows': []}
try:
dbname = get_module_dbname('contract_management')
async with DBPools().sqlorContext(dbname) as sor:
ns = {'page': 1, 'rows': 50, 'sort': 'COLUMN_NAME'}
sql = "SELECT COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='crm_db' AND TABLE_NAME='contract'"
rows = await sor.sqlExe(sql, ns)
if isinstance(rows, dict):
rows = rows.get('rows', [])
if rows:
result['keys'] = list(dict(rows[0]).keys())
result['rows'] = [list(dict(r).values()) for r in rows]
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Contract list API"""
import json
result = {'success': False, 'rows': [], 'total': 0}
try:
dbname = get_module_dbname('contract_management')
ns = {
'page': int(params_kw.get('page', 1)),
'rows': int(params_kw.get('rows', 20)),
'sort': 'created_at desc'
}
sql = "SELECT id, contract_number, title, party_a, party_b, contract_type, status, amount, start_date, end_date, sign_date, owner_id, created_at FROM contract"
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)

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Contract create API"""
import json, uuid, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
try:
contract_number = params_kw.get('contract_number', '').strip()
title = params_kw.get('title', '').strip()
party_a = params_kw.get('party_a', '').strip()
party_b = params_kw.get('party_b', '').strip()
contract_type = params_kw.get('contract_type', '').strip()
amount = params_kw.get('amount', '0').strip()
start_date = params_kw.get('start_date', '').strip() or None
end_date = params_kw.get('end_date', '').strip() or None
owner_id = params_kw.get('owner_id', '').strip()
if not contract_number or not title or not party_a or not party_b or not owner_id:
result['options'] = {'title': 'Error', 'message': '请填写必填字段', 'type': 'error'}
else:
dbname = get_module_dbname('contract_management')
new_id = str(uuid.uuid4()).replace('-', '')
now = time.strftime('%Y-%m-%d %H:%M:%S')
sign_date = params_kw.get('sign_date', '').strip() or None
org_id = params_kw.get('org_id', '').strip() or 'org001'
async with DBPools().sqlorContext(dbname) as sor:
existing = await sor.sqlExe("SELECT id FROM contract WHERE contract_number = ${contract_number}$", {'contract_number': contract_number})
if existing:
result['options'] = {'title': 'Error', 'message': '合同编号已存在', 'type': 'error'}
else:
await sor.sqlExe("""INSERT INTO contract (id, contract_number, title, party_a, party_b, contract_type, amount, start_date, end_date, sign_date, status, owner_id, org_id, created_at)
VALUES (${id}$, ${contract_number}$, ${title}$, ${party_a}$, ${party_b}$, ${contract_type}$, ${amount}$, ${start_date}$, ${end_date}$, ${sign_date}$, ${status}$, ${owner_id}$, ${org_id}$, ${created_at}$)""", {
'id': new_id, 'contract_number': contract_number, 'title': title,
'party_a': party_a, 'party_b': party_b, 'contract_type': contract_type,
'amount': float(amount) if amount else 0,
'start_date': start_date, 'end_date': end_date,
'sign_date': sign_date,
'status': params_kw.get('status', 'draft').strip(),
'owner_id': owner_id, 'org_id': org_id,
'created_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)

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Contract 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('contract_management')
async with DBPools().sqlorContext(dbname) as sor:
existing = await sor.sqlExe("SELECT id FROM contract WHERE id = ${id}$", {'id': record_id})
if not existing:
result['options'] = {'title': 'Error', 'message': '合同不存在', 'type': 'error'}
else:
await sor.sqlExe("DELETE FROM contract 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)

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Contract 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('contract_management')
now = time.strftime('%Y-%m-%d %H:%M:%S')
fields = ['contract_number', 'title', 'party_a', 'party_b', 'contract_type', 'amount', 'start_date', 'end_date', 'sign_date', 'status', 'owner_id', '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("created_at = ${created_at}$")
params['created_at'] = now
if len(set_parts) <= 1:
result['options'] = {'title': 'Error', 'message': '没有可更新的字段', 'type': 'error'}
else:
sql = f"UPDATE contract 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)

View File

@ -56,7 +56,7 @@
"options": { "options": {
"url": "{{entire_url('contract_edit.ui')}}", "url": "{{entire_url('contract_edit.ui')}}",
"params": { "params": {
"id": "{{params_kw.get('contract_id')}}" "id": "{% raw %}{{params_kw.get('contract_id')}}{% endraw %}"
} }
} }
} }
@ -74,7 +74,7 @@
"widgettype": "Form", "widgettype": "Form",
"options": { "options": {
"tblname": "contract", "tblname": "contract",
"record_id": "{{params_kw.get('contract_id')}}", "record_id": "{% raw %}{{params_kw.get('contract_id')}}{% endraw %}",
"readonly": true, "readonly": true,
"width": "100%", "width": "100%",
"style": { "style": {
@ -122,7 +122,7 @@
"tblname": "contract_attachment", "tblname": "contract_attachment",
"width": "100%", "width": "100%",
"parent_field": "contract_id", "parent_field": "contract_id",
"parent_value": "{{params_kw.get('contract_id')}}" "parent_value": "{% raw %}{{params_kw.get('contract_id')}}{% endraw %}"
} }
} }
] ]
@ -164,7 +164,7 @@
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"fieldname": "ai_compliance_result", "fieldname": "ai_compliance_result",
"record_id": "{{params_kw.get('contract_id')}}", "record_id": "{% raw %}{{params_kw.get('contract_id')}}{% endraw %}",
"tblname": "contract", "tblname": "contract",
"style": { "style": {
"whiteSpace": "pre-wrap", "whiteSpace": "pre-wrap",
@ -194,7 +194,7 @@
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"fieldname": "ai_key_dates", "fieldname": "ai_key_dates",
"record_id": "{{params_kw.get('contract_id')}}", "record_id": "{% raw %}{{params_kw.get('contract_id')}}{% endraw %}",
"tblname": "contract", "tblname": "contract",
"style": { "style": {
"whiteSpace": "pre-wrap", "whiteSpace": "pre-wrap",

View File

@ -1,150 +1,51 @@
{
"widgettype": "Page",
"options": {
"title": "合同编辑",
"style": {"height": "100vh", "padding": "0"}
},
"subwidgets": [
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {"style": {"padding": "16px", "flex": 1, "overflow": "auto"}},
"width": "100%",
"height": "100%"
},
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "Form", "widgettype": "Form",
"id": "contract_form",
"options": { "options": {
"title": "合同信息", "submit_url": "{{entire_url('api/contracts_create.dspy')}}",
"tblname": "contract", "method": "POST",
"submit_url": "/api/contract/save", "layout": "vertical",
"width": "100%", "style": {"maxWidth": "600px"},
"style": { "fields": [
"maxWidth": "1200px", {"name": "contract_number", "label": "合同编号", "uitype": "text", "required": true},
"margin": "0 auto" {"name": "title", "label": "合同名称", "uitype": "text", "required": true},
} {"name": "party_a", "label": "甲方", "uitype": "text", "required": true},
}, {"name": "party_b", "label": "乙方", "uitype": "text", "required": true},
"binds": [ {"name": "contract_type", "label": "合同类型", "uitype": "code", "required": true, "data": [
{ {"value": "sales", "text": "销售合同"},
"wid": "self", {"value": "purchase", "text": "采购合同"},
"event": "submited", {"value": "service", "text": "服务合同"},
"actiontype": "script", {"value": "other", "text": "其他"}
"script": "await bricks.show_resp_message_or_error(event.params); if (event.params.success) { bricks.app.goto_url('{{entire_url(\"contract_list.ui\")}}'); }" ]},
} {"name": "amount", "label": "合同金额", "uitype": "number"},
{"name": "start_date", "label": "开始日期", "uitype": "date", "required": true},
{"name": "end_date", "label": "结束日期", "uitype": "date", "required": true},
{"name": "sign_date", "label": "签署日期", "uitype": "date"},
{"name": "owner_id", "label": "负责人ID", "uitype": "text", "required": true},
{"name": "org_id", "label": "组织ID", "uitype": "text"},
{"name": "status", "label": "状态", "uitype": "code", "data": [
{"value": "draft", "text": "草稿"},
{"value": "active", "text": "生效"},
{"value": "expired", "text": "过期"},
{"value": "terminated", "text": "终止"}
]}
],
"buttons": [
{"type": "submit", "text": "保存", "variant": "primary"},
{"type": "button", "text": "取消", "action": "navigate('main/contract_management/contract_list.ui')"}
] ]
},
{
"widgettype": "Filler",
"options": {
"height": "20px"
} }
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"style": {
"maxWidth": "1200px",
"margin": "0 auto"
}
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"width": "50%"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "合同附件",
"style": {
"fontSize": "18px",
"fontWeight": "600",
"marginBottom": "10px"
}
}
},
{
"widgettype": "DataGrid",
"options": {
"tblname": "contract_attachment",
"width": "100%",
"parent_field": "contract_id",
"parent_value": "{{params_kw.get('id') or 'new'}}"
}
}
]
},
{
"widgettype": "Filler",
"options": {
"width": "20px"
}
},
{
"widgettype": "VBox",
"options": {
"width": "50%"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "AI分析结果",
"style": {
"fontSize": "18px",
"fontWeight": "600",
"marginBottom": "10px"
}
}
},
{
"widgettype": "Button",
"options": {
"text": "执行合规检查",
"styleType": "primary",
"style": {
"marginBottom": "10px"
}
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "console.log('AI合规检查功能待实现')"
}
]
},
{
"widgettype": "Button",
"options": {
"text": "提取关键时点",
"styleType": "primary",
"style": {
"marginBottom": "10px"
}
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "console.log('AI关键时点提取功能待实现')"
}
]
},
{
"widgettype": "Button",
"options": {
"text": "版本对比",
"styleType": "primary"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "console.log('AI版本对比功能待实现')"
}
]
}
]
} }
] ]
} }

View File

@ -0,0 +1,57 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""List contract"""
import json
result = {'success': False, 'rows': [], 'total': 0}
try:
dbname = get_module_dbname('contract_management')
async with DBPools().sqlorContext(dbname) as sor:
where_clauses = []
where_ns = {}
customer_id = params_kw.get('customer_id', '')
status = params_kw.get('status', '')
if customer_id:
where_clauses.append("customer_id=${customer_id}$")
where_ns['customer_id'] = customer_id
if status:
where_clauses.append("status=${status}$")
where_ns['status'] = status
where_sql = " AND ".join(where_clauses)
where_prefix = " WHERE " if where_clauses else ""
count_sql = f"SELECT count(*) rcnt FROM contract{where_prefix}{where_sql}"
count_rows = await sor.sqlExe(count_sql, where_ns)
total = 0
if count_rows and len(count_rows) > 0:
r = count_rows[0]
if hasattr(r, 'keys'):
total = r.get('rcnt', 0)
elif isinstance(r, dict):
total = r.get('rcnt', 0)
elif hasattr(r, 'rcnt'):
total = r.rcnt
if total > 0:
ns = {'page': int(params_kw.get('page', 1)), 'rows': int(params_kw.get('rows', 20)), 'sort': params_kw.get('sort', 'created_at')}
sql = f"SELECT id, title, contract_number, contract_type, customer_id, amount, sign_date, start_date, end_date, status, owner_id, org_id, created_at, updated_at FROM contract{where_prefix}{where_sql}"
query_ns = dict(list(ns.items()) + list(where_ns.items()))
rows = await sor.sqlExe(sql, query_ns)
if isinstance(rows, dict):
result['rows'] = rows.get('rows', [])
result['total'] = rows.get('total', total)
elif rows:
result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows]
result['total'] = total
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -1,81 +1,45 @@
{
"widgettype": "Page",
"options": {
"title": "合同管理",
"style": {"height": "100vh", "padding": "0"}
},
"subwidgets": [
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": { "options": {"style": {"padding": "16px", "flex": 1, "overflow": "hidden"}},
"width": "100%",
"height": "100%"
},
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "HBox", "widgettype": "HBox",
"options": { "options": {"style": {"marginBottom": "16px", "gap": "8px"}},
"width": "100%",
"height": "50px"
},
"subwidgets": [ "subwidgets": [
{ {"widgettype": "TextField", "id": "search_keyword", "options": {"label": "搜索", "placeholder": "合同编号/名称", "style": {"flex": 1}}},
"widgettype": "Text", {"widgettype": "Button", "id": "btn_search", "options": {"text": "搜索", "variant": "primary"}},
"options": { {"widgettype": "Button", "id": "btn_add", "options": {"text": "新增", "variant": "primary", "action": "navigate('main/contract_management/contract_edit.ui')"}}
"text": "合同管理",
"style": {
"fontSize": "24px",
"fontWeight": "600",
"marginLeft": "20px",
"marginTop": "10px"
}
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Button",
"options": {
"text": "新建合同",
"styleType": "primary",
"style": {
"marginRight": "20px",
"marginTop": "8px"
}
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "main_content",
"options": {
"url": "{{entire_url('contract_edit.ui')}}"
}
}
]
}
] ]
}, },
{ {
"widgettype": "Filler",
"options": {
"height": "10px"
}
},
{
"id": "main_content",
"widgettype": "DataGrid", "widgettype": "DataGrid",
"id": "contract_grid",
"options": { "options": {
"tblname": "contract", "url": "{{entire_url('api/contract_list.dspy')}}",
"width": "100%", "style": {"flex": 1},
"height": "calc(100% - 70px)" "columns": [
}, {"field": "contract_number", "header": "合同编号", "width": 140},
"binds": [ {"field": "title", "header": "合同名称", "width": 200},
{ {"field": "party_a", "header": "甲方", "width": 150},
"wid": "self", {"field": "party_b", "header": "乙方", "width": 150},
"event": "rowSelected", {"field": "contract_type", "header": "类型", "width": 100},
"actiontype": "urlwidget", {"field": "amount", "header": "金额", "width": 120},
"target": "main_content", {"field": "start_date", "header": "开始日期", "width": 110},
"options": { {"field": "end_date", "header": "结束日期", "width": 110},
"url": "{{entire_url('contract_detail.ui')}}", {"field": "status", "header": "状态", "width": 90}
"params": { ],
"contract_id": "{{params.row.id}}" "toolbar": [
} {"type": "button", "text": "详情", "icon": "view", "action": "navigate('main/contract_management/contract_detail.ui?id={% raw %}{{selectedRow.id}}{% endraw %}')"},
{"type": "button", "text": "编辑", "icon": "edit", "action": "navigate('main/contract_management/contract_edit.ui?id={% raw %}{{selectedRow.id}}{% endraw %}')"},
{"type": "button", "text": "删除", "icon": "delete", "action": "doDelete('{% raw %}{{selectedRow.id}}{% endraw %}')"}
]
} }
} }
] ]