From e3c19bc359b851ff748b7fa50437e134720b9597 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 28 Apr 2026 18:53:13 +0800 Subject: [PATCH] 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 --- financial_management/financial_core.py | 525 ++++++------------------- models/receivables.json | 168 +------- mysql.ddl.sql | 82 ++++ wwwroot/api/debug_receivables.dspy | 18 + wwwroot/api/receivables.dspy | 30 ++ wwwroot/api/receivables_create.dspy | 38 ++ wwwroot/api/receivables_delete.dspy | 25 ++ wwwroot/api/receivables_list.dspy | 23 ++ wwwroot/api/receivables_update.dspy | 39 ++ wwwroot/api/test_env.dspy | 14 + wwwroot/financial_vouchers.ui | 30 ++ wwwroot/index.ui | 211 ++++++++++ wwwroot/payments.ui | 30 ++ wwwroot/receipts.ui | 30 ++ wwwroot/receivable_edit.ui | 45 +++ wwwroot/receivables.ui | 46 +++ 16 files changed, 808 insertions(+), 546 deletions(-) create mode 100644 mysql.ddl.sql create mode 100644 wwwroot/api/debug_receivables.dspy create mode 100644 wwwroot/api/receivables.dspy create mode 100644 wwwroot/api/receivables_create.dspy create mode 100644 wwwroot/api/receivables_delete.dspy create mode 100644 wwwroot/api/receivables_list.dspy create mode 100644 wwwroot/api/receivables_update.dspy create mode 100644 wwwroot/api/test_env.dspy create mode 100644 wwwroot/financial_vouchers.ui create mode 100644 wwwroot/index.ui create mode 100644 wwwroot/payments.ui create mode 100644 wwwroot/receipts.ui create mode 100644 wwwroot/receivable_edit.ui create mode 100644 wwwroot/receivables.ui diff --git a/financial_management/financial_core.py b/financial_management/financial_core.py index 39f3f7c..5c44f1a 100644 --- a/financial_management/financial_core.py +++ b/financial_management/financial_core.py @@ -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_db_connection(self, org_id: str): - """Get database connection for organization""" - dburl = self.config.get('dburl') - return await getDBP(dburl, {'org_id': org_id}) - - async def create_receivable_from_order(self, order_id: str, org_id: str) -> str: - """自动立应收:按订单生成应收计划""" - dbp = await self.get_db_connection(org_id) - - # 获取订单信息(假设订单管理模块已存在) - order = await dbp.select_one("orders", {"id": order_id, "org_id": org_id}) - if not order: - raise ValueError(f"订单 {order_id} 不存在") - - # 计算应收日期和到期日期 - receivable_date = order.get("acceptance_deadline") or order.get("created_at") - if isinstance(receivable_date, str): - receivable_date = datetime.strptime(receivable_date, "%Y-%m-%d").date() - elif isinstance(receivable_date, datetime): - receivable_date = receivable_date.date() - - credit_period = int(order.get("credit_period", 0)) if order.get("credit_period") else 0 - due_date = receivable_date + timedelta(days=credit_period) if credit_period > 0 else None - - # 创建应收记录 - receivable_id = str(uuid.uuid4()).replace('-', '') - receivable_data = { - "id": receivable_id, - "order_id": order_id, - "contract_id": order["contract_id"], - "customer_id": order["customer_id"], - "receivable_amount": order["amount"], - "received_amount": Decimal("0.00"), - "receivable_date": receivable_date, - "due_date": due_date, - "credit_period": credit_period, - "status": "pending", - "sales_owner_id": order.get("owner_id"), - "org_id": org_id, - "created_at": datetime.now(), - "updated_at": datetime.now() - } - - await dbp.insert("receivables", receivable_data) - return receivable_id - - async def create_receipt(self, receipt_data: Dict, user_id: str, org_id: str) -> str: - """收款录入""" - dbp = await self.get_db_connection(org_id) - - # 验证关联的订单和金额 - allocations = receipt_data.get("allocations", []) - total_allocated = Decimal("0.00") - - for alloc in allocations: - order_id = alloc["order_id"] - allocated_amount = Decimal(str(alloc["allocated_amount"])) - - # 验证订单存在且属于当前组织 - order = await dbp.select_one("orders", {"id": order_id, "org_id": org_id}) - if not order: - raise ValueError(f"订单 {order_id} 不存在或不属于当前组织") - - # 验证应收记录存在 - receivable = await dbp.select_one("receivables", {"order_id": order_id, "org_id": org_id}) - if not receivable: - raise ValueError(f"订单 {order_id} 没有对应的应收记录") - - # 验证分配金额不超过应收金额 - remaining_amount = Decimal(str(receivable["receivable_amount"])) - Decimal(str(receivable["received_amount"])) - if allocated_amount > remaining_amount: - raise ValueError(f"订单 {order_id} 分配金额 {allocated_amount} 超过剩余应收金额 {remaining_amount}") - - total_allocated += allocated_amount - - # 验证总分配金额等于收款总额 - total_receipt_amount = Decimal(str(receipt_data["total_amount"])) - if abs(total_allocated - total_receipt_amount) > Decimal("0.01"): # 允许小数点误差 - raise ValueError(f"分配总金额 {total_allocated} 与收款总额 {total_receipt_amount} 不匹配") - - # 创建收款记录 - receipt_id = str(uuid.uuid4()).replace('-', '') - receipt_number = f"REC-{datetime.now().strftime('%Y%m%d')}-{receipt_id[:8].upper()}" - final_receipt_data = { - "id": receipt_id, - "receipt_number": receipt_number, - "customer_id": receipt_data["customer_id"], - "total_amount": total_receipt_amount, - "receipt_date": receipt_data["receipt_date"], - "receipt_method": receipt_data.get("receipt_method", "bank_transfer"), - "receipt_status": "processed", - "description": receipt_data.get("description"), - "created_by": user_id, - "org_id": org_id, - "created_at": datetime.now(), - "updated_at": datetime.now() - } - - await dbp.insert("receipts", final_receipt_data) - - # 创建收款分配记录 - for alloc in allocations: - allocation_id = str(uuid.uuid4()).replace('-', '') - receivable = await dbp.select_one("receivables", {"order_id": alloc["order_id"], "org_id": org_id}) - - allocation_data = { - "id": allocation_id, - "receipt_id": receipt_id, - "order_id": alloc["order_id"], - "receivable_id": receivable["id"], - "allocated_amount": Decimal(str(alloc["allocated_amount"])), - "allocation_percentage": Decimal(str(alloc.get("allocation_percentage", "0"))) if alloc.get("allocation_percentage") else None, - "contract_id": receivable["contract_id"], - "org_id": org_id, - "created_at": datetime.now() - } - - await dbp.insert("receipt_allocations", allocation_data) - - # 更新应收记录的已收金额 - new_received_amount = Decimal(str(receivable["received_amount"])) + Decimal(str(alloc["allocated_amount"])) - new_status = "completed" if new_received_amount >= Decimal(str(receivable["receivable_amount"])) else "partial" - - await dbp.update( - "receivables", - { - "received_amount": new_received_amount, - "status": new_status, - "updated_at": datetime.now() - }, - {"id": receivable["id"]} - ) - - # 如果订单已完成收款,更新订单状态并触发合同履约 - if new_status == "completed": - await self._update_order_and_contract_fulfillment(alloc["order_id"], receivable["contract_id"], org_id) - - # 生成财务凭证 - await self._generate_financial_vouchers(receipt_id, org_id) - - return receipt_id - - async def _update_order_and_contract_fulfillment(self, order_id: str, contract_id: str, org_id: str): - """更新订单状态并触发合同履约""" - dbp = await self.get_db_connection(org_id) - - # 更新订单状态为已收款 - await dbp.update( - "orders", - {"status": "paid", "updated_at": datetime.now()}, - {"id": order_id, "org_id": org_id} - ) - - # 触发合同履约进度更新(这里应该调用合同管理模块) - # 简化实现:更新合同里程碑状态 - milestone_type_map = { - "advance": "预付款到账", - "progress": "进度款到账", - "final": "尾款到账", - "acceptance": "验收款到账" - } - - order = await dbp.select_one("orders", {"id": order_id, "org_id": org_id}) - if order: - order_type = order.get("order_type", "other") - milestone_name = milestone_type_map.get(order_type, f"{order_type}到账") - - # 查找对应的里程碑并标记为完成 - milestones = await dbp.query( - "SELECT id FROM contract_milestones WHERE contract_id = %(contract_id)s AND milestone_name LIKE %(milestone_name)s", - {"contract_id": contract_id, "milestone_name": f"%{milestone_name}%"} - ) - - for milestone in milestones: - await dbp.update( - "contract_milestones", - {"status": "completed", "actual_date": datetime.now().date(), "updated_at": datetime.now()}, - {"id": milestone["id"]} - ) - - async def _generate_financial_vouchers(self, receipt_id: str, org_id: str): - """生成财务凭证""" - dbp = await self.get_db_connection(org_id) - - # 获取收款记录 - receipt = await dbp.select_one("receipts", {"id": receipt_id, "org_id": org_id}) - if not receipt: - return - - # 获取分配记录 - allocations = await dbp.query( - "SELECT * FROM receipt_allocations WHERE receipt_id = %(receipt_id)s AND org_id = %(org_id)s", - {"receipt_id": receipt_id, "org_id": org_id} - ) - - for alloc in allocations: - voucher_id = str(uuid.uuid4()).replace('-', '') - voucher_number = f"VOU-{datetime.now().strftime('%Y%m%d')}-{voucher_id[:8].upper()}" - - # 获取合同和订单信息用于凭证描述 - contract = await dbp.select_one("contract", {"id": alloc["contract_id"], "org_id": org_id}) - order = await dbp.select_one("orders", {"id": alloc["order_id"], "org_id": org_id}) - - contract_number = contract["contract_number"] if contract else "UNKNOWN" - order_number = order["order_number"] if order else "UNKNOWN" - - description = f"合同 {contract_number} - 订单 {order_number} 收款凭证" - - voucher_data = { - "id": voucher_id, - "voucher_number": voucher_number, - "voucher_type": "receipt", - "contract_id": alloc["contract_id"], - "order_id": alloc["order_id"], - "amount": alloc["allocated_amount"], - "voucher_date": receipt["receipt_date"], - "description": description, - "reference_id": receipt_id, - "org_id": org_id, - "created_at": datetime.now() - } - - await dbp.insert("financial_vouchers", voucher_data) - - async def get_contract_financial_summary(self, contract_id: str, org_id: str) -> Dict: - """获取合同层面的财务数据汇总""" - dbp = await self.get_db_connection(org_id) - - # 获取合同总金额 - contract = await dbp.select_one("contract", {"id": contract_id, "org_id": org_id}) - if not contract: - return {} - - contract_amount = Decimal(str(contract["amount"])) - - # 获取所有关联订单的应收和已收金额 - sql = """ - SELECT - SUM(r.receivable_amount) as total_receivable, - SUM(r.received_amount) as total_received - FROM receivables r - JOIN orders o ON r.order_id = o.id - WHERE o.contract_id = %(contract_id)s - AND r.org_id = %(org_id)s - """ - - result = await dbp.doQuery(sql, {"contract_id": contract_id, "org_id": org_id}) - - if result and result[0]["total_receivable"]: - total_receivable = Decimal(str(result[0]["total_receivable"])) - total_received = Decimal(str(result[0]["total_received"])) if result[0]["total_received"] else Decimal("0.00") - total_remaining = total_receivable - total_received - completion_rate = (total_received / contract_amount * 100) if contract_amount > 0 else Decimal("0.00") - else: - total_receivable = Decimal("0.00") - total_received = Decimal("0.00") - total_remaining = Decimal("0.00") - completion_rate = Decimal("0.00") - - return { - "contract_total_amount": contract_amount, - "total_receivable_amount": total_receivable, - "total_received_amount": total_received, - "total_remaining_amount": total_remaining, - "completion_rate": float(completion_rate), - "contract_id": contract_id - } - - async def get_overdue_receivables(self, org_id: str, days_overdue: int = 30) -> List[Dict]: - """获取逾期应收记录(超期指定天数)""" - dbp = await self.get_db_connection(org_id) - - sql = """ - SELECT - r.*, - o.order_number, - c.contract_number, - cu.name as customer_name, - u.username as sales_owner_name - FROM receivables r - JOIN orders o ON r.order_id = o.id - JOIN contract c ON r.contract_id = c.id - JOIN customers cu ON r.customer_id = cu.id - LEFT JOIN users u ON r.sales_owner_id = u.id - WHERE r.org_id = %(org_id)s - AND r.status IN ('pending', 'partial') - AND r.due_date IS NOT NULL - AND r.due_date <= DATE_SUB(CURDATE(), INTERVAL %(days)s DAY) - ORDER BY r.due_date ASC - """ - - overdue_receivables = await dbp.doQuery(sql, {"org_id": org_id, "days": days_overdue}) - return overdue_receivables - - async def send_overdue_notifications(self, org_id: str, days_overdue: int = 30) -> int: - """发送逾期通知给销售和财务""" - overdue_receivables = await self.get_overdue_receivables(org_id, days_overdue) - - # 这里应该集成消息通知系统 - # 目前只记录日志,实际应用中应发送邮件/站内信等 - notification_count = len(overdue_receivables) - - if notification_count > 0: - print(f"发现 {notification_count} 个逾期应收记录需要通知") - for receivable in overdue_receivables: - print(f"通知: 销售 {receivable.get('sales_owner_name', '未知')} 和财务 - " - f"合同 {receivable['contract_number']} 订单 {receivable['order_number']} " - f"逾期 {datetime.now().date() - receivable['due_date']} 天") - - return notification_count - - async def create_payment(self, payment_data: Dict, user_id: str, org_id: str) -> str: - """创建支出记录""" - dbp = await self.get_db_connection(org_id) - - # 验证关联合同的收款已核销 - contract_id = payment_data["contract_id"] - contract_summary = await self.get_contract_financial_summary(contract_id, org_id) - - if contract_summary.get("total_received_amount", Decimal("0.00")) <= Decimal("0.00"): - raise ValueError("关联合同没有已核销的收款,不能创建支出") - - # 创建支出记录 - payment_id = str(uuid.uuid4()).replace('-', '') - payment_number = f"PMT-{datetime.now().strftime('%Y%m%d')}-{payment_id[:8].upper()}" - final_payment_data = { - "id": payment_id, - "payment_number": payment_number, - "contract_id": contract_id, - "vendor_id": payment_data["vendor_id"], - "payment_amount": Decimal(str(payment_data["payment_amount"])), - "payment_date": payment_data["payment_date"], - "payment_method": payment_data.get("payment_method", "bank_transfer"), - "payment_status": "processed", - "description": payment_data.get("description"), - "approved_by": payment_data.get("approved_by"), - "created_by": user_id, - "org_id": org_id, - "created_at": datetime.now(), - "updated_at": datetime.now() - } - - await dbp.insert("payments", final_payment_data) - - # 生成支出凭证 - await self._generate_payment_voucher(payment_id, org_id) - - return payment_id - - async def _generate_payment_voucher(self, payment_id: str, org_id: str): - """生成支出凭证""" - dbp = await self.get_db_connection(org_id) - - payment = await dbp.select_one("payments", {"id": payment_id, "org_id": org_id}) - if not payment: - return - - contract = await dbp.select_one("contract", {"id": payment["contract_id"], "org_id": org_id}) - contract_number = contract["contract_number"] if contract else "UNKNOWN" - - voucher_id = str(uuid.uuid4()).replace('-', '') - voucher_number = f"VOU-{datetime.now().strftime('%Y%m%d')}-{voucher_id[:8].upper()}" - description = f"合同 {contract_number} 支出凭证" - - voucher_data = { - "id": voucher_id, - "voucher_number": voucher_number, - "voucher_type": "payment", - "contract_id": payment["contract_id"], - "order_id": None, # 支出通常不关联具体订单 - "amount": payment["payment_amount"], - "voucher_date": payment["payment_date"], - "description": description, - "reference_id": payment_id, - "org_id": org_id, - "created_at": datetime.now() - } - - await dbp.insert("financial_vouchers", voucher_data) -# Global instance -financial_manager = FinancialManager() +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) + -# 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 with await get_financial_db() as sor: + # 获取订单信息 + ns = {'page': 1, 'rows': 1, 'sort': 'id'} + orders = await sor.sqlExe( + "SELECT id, contract_id, customer_id, amount, owner_id FROM orders WHERE id=${id}$ AND org_id=${org_id}$", + {'id': order_id, 'org_id': org_id} + ) + if not orders: + raise ValueError(f"订单 {order_id} 不存在") + + order = dict(orders[0]) if hasattr(orders[0], 'keys') else orders[0] + + # 计算到期日期(使用订单创建日期+30天默认账期) + created_at = order.get('created_at') + if isinstance(created_at, str): + created_at = datetime.strptime(created_at, "%Y-%m-%d %H:%M:%S") + due_date = (created_at + timedelta(days=30)).date() if created_at else None + + receivable_id = str(uuid.uuid4()).replace('-', '')[:32] + + await sor.sqlExe(""" + INSERT INTO receivables (id, order_id, contract_id, customer_id, receivable_amount, received_amount, due_date, status, org_id, created_at, updated_at) + VALUES (${id}$, ${order_id}$, ${contract_id}$, ${customer_id}$, ${amount}$, 0, ${due_date}$, 'pending', ${org_id}$, NOW(), NOW()) + """, { + 'id': receivable_id, + 'order_id': order_id, + 'contract_id': order.get('contract_id', ''), + 'customer_id': order.get('customer_id', ''), + 'amount': order.get('amount', 0), + 'due_date': due_date.isoformat() if due_date else None, + 'org_id': org_id + }) + + return receivable_id + async def create_receipt(receipt_data: Dict, user_id: str, org_id: str) -> str: - return await financial_manager.create_receipt(receipt_data, user_id, org_id) + """收款录入""" + async with await get_financial_db() as sor: + receipt_id = str(uuid.uuid4()).replace('-', '')[:32] + + await sor.sqlExe(""" + INSERT INTO receipts (id, customer_id, total_amount, receipt_date, receipt_method, status, org_id, created_by, created_at, updated_at) + VALUES (${id}$, ${customer_id}$, ${total_amount}$, ${receipt_date}$, ${method}$, 'processed', ${org_id}$, ${user_id}$, NOW(), NOW()) + """, { + 'id': receipt_id, + 'customer_id': receipt_data.get('customer_id', ''), + 'total_amount': receipt_data.get('total_amount', 0), + 'receipt_date': receipt_data.get('receipt_date', datetime.now().date().isoformat()), + 'method': receipt_data.get('receipt_method', 'bank_transfer'), + 'org_id': org_id, + 'user_id': user_id + }) + + return receipt_id + async def get_contract_financial_summary(contract_id: str, org_id: str) -> Dict: - return await financial_manager.get_contract_financial_summary(contract_id, org_id) + """获取合同层面的财务数据汇总""" + async with await get_financial_db() as sor: + ns = {'page': 1, 'rows': 50, 'sort': 'id'} + sql = """ + SELECT + SUM(r.receivable_amount) as total_receivable, + SUM(r.received_amount) as total_received + FROM receivables r + WHERE r.contract_id = ${contract_id}$ AND r.org_id = ${org_id}$ + """ + rows = await sor.sqlExe(sql, {'contract_id': contract_id, 'org_id': org_id}) + + result = {'contract_id': contract_id, 'total_receivable': 0, 'total_received': 0, 'total_remaining': 0} + if rows: + r = dict(rows[0]) if hasattr(rows[0], 'keys') else rows[0] + result['total_receivable'] = float(r.get('total_receivable', 0) or 0) + result['total_received'] = float(r.get('total_received', 0) or 0) + result['total_remaining'] = result['total_receivable'] - result['total_received'] + + return result + async def get_overdue_receivables(org_id: str, days_overdue: int = 30) -> List[Dict]: - 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) \ No newline at end of file + """创建支出记录""" + 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 diff --git a/models/receivables.json b/models/receivables.json index bb691ed..5f7e6fa 100644 --- a/models/receivables.json +++ b/models/receivables.json @@ -1,149 +1,23 @@ { - "summary": { - "tablename": "receivables", - "label": "应收记录", - "comment": "按订单维度的应收记录管理" - }, - "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": "更新时间" - } - ], - "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": [] + "table_name": "receivables", + "fields": [ + {"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_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"]} + ] } \ No newline at end of file diff --git a/mysql.ddl.sql b/mysql.ddl.sql new file mode 100644 index 0000000..1a4d4f5 --- /dev/null +++ b/mysql.ddl.sql @@ -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='收款分配'; diff --git a/wwwroot/api/debug_receivables.dspy b/wwwroot/api/debug_receivables.dspy new file mode 100644 index 0000000..dedf8d7 --- /dev/null +++ b/wwwroot/api/debug_receivables.dspy @@ -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) diff --git a/wwwroot/api/receivables.dspy b/wwwroot/api/receivables.dspy new file mode 100644 index 0000000..b5f0d08 --- /dev/null +++ b/wwwroot/api/receivables.dspy @@ -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) diff --git a/wwwroot/api/receivables_create.dspy b/wwwroot/api/receivables_create.dspy new file mode 100644 index 0000000..95297f5 --- /dev/null +++ b/wwwroot/api/receivables_create.dspy @@ -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) diff --git a/wwwroot/api/receivables_delete.dspy b/wwwroot/api/receivables_delete.dspy new file mode 100644 index 0000000..4bea5d8 --- /dev/null +++ b/wwwroot/api/receivables_delete.dspy @@ -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) diff --git a/wwwroot/api/receivables_list.dspy b/wwwroot/api/receivables_list.dspy new file mode 100644 index 0000000..0135267 --- /dev/null +++ b/wwwroot/api/receivables_list.dspy @@ -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) diff --git a/wwwroot/api/receivables_update.dspy b/wwwroot/api/receivables_update.dspy new file mode 100644 index 0000000..2000953 --- /dev/null +++ b/wwwroot/api/receivables_update.dspy @@ -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) diff --git a/wwwroot/api/test_env.dspy b/wwwroot/api/test_env.dspy new file mode 100644 index 0000000..66f4681 --- /dev/null +++ b/wwwroot/api/test_env.dspy @@ -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) diff --git a/wwwroot/financial_vouchers.ui b/wwwroot/financial_vouchers.ui new file mode 100644 index 0000000..e966284 --- /dev/null +++ b/wwwroot/financial_vouchers.ui @@ -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" + } + } + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..0c35017 --- /dev/null +++ b/wwwroot/index.ui @@ -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)" + } + } + ] +} diff --git a/wwwroot/payments.ui b/wwwroot/payments.ui new file mode 100644 index 0000000..7f31818 --- /dev/null +++ b/wwwroot/payments.ui @@ -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" + } + } + ] +} diff --git a/wwwroot/receipts.ui b/wwwroot/receipts.ui new file mode 100644 index 0000000..d28d113 --- /dev/null +++ b/wwwroot/receipts.ui @@ -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" + } + } + ] +} diff --git a/wwwroot/receivable_edit.ui b/wwwroot/receivable_edit.ui new file mode 100644 index 0000000..0248dc4 --- /dev/null +++ b/wwwroot/receivable_edit.ui @@ -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')"} + ] + } + } + ] + } + ] +} diff --git a/wwwroot/receivables.ui b/wwwroot/receivables.ui new file mode 100644 index 0000000..2d4c90e --- /dev/null +++ b/wwwroot/receivables.ui @@ -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 %}')"} + ] + } + } + ] + } + ] +}