Merge branch 'main' of git.opencomputing.cn:yumoqing/accounting

# Conflicts:
#	wwwroot/index.ui
#	wwwroot/index.ui~be97eaf7b5edfa4867c9caa6a5e0ffd1e499faa3
This commit is contained in:
yumoqing 2026-05-28 19:08:47 +08:00
commit e34be6ad16
17 changed files with 1078 additions and 8 deletions

View File

@ -7,6 +7,7 @@ from .accounting_config import Accounting
from .bill import write_bill
from .openaccount import openOwnerAccounts, openProviderAccounts, openResellerAccounts, openCustomerAccounts, openRetailRelationshipAccounts
from .getaccount import getAccountBalance, getCustomerBalance, getAccountByName, get_account_total_amount
from .stats import get_accounting_stats
from .recharge import RechargeBiz, recharge_accounting
from .consume import consume_accounting
@ -19,16 +20,10 @@ async def all_my_accounts(request):
b.id,
a.name,
b.balance_at,
case when c.balance is null then 0.00 else c.balance end as balance
b.balance
from
subject a,
account b left join
(
select a.*
from acc_balance a,
(select accountid, max(acc_date) max_date from acc_balance group by accountid) b
where a.accountid=b.accountid and a.acc_date=b.max_date
) c on c.accountid = b.id
account b
where b.subjectid = a.id
and b.orgid = ${orgid}$
"""
@ -77,3 +72,4 @@ def load_accounting():
g.get_accdetail = get_accdetail
g.all_my_accounts = all_my_accounts
g.openRetailRelationshipAccounts = openRetailRelationshipAccounts
g.get_accounting_stats = get_accounting_stats

77
accounting/stats.py Normal file
View File

@ -0,0 +1,77 @@
from appPublic.log import debug, exception
from sqlor.dbpools import get_sor_context
from datetime import datetime, timedelta
async def get_accounting_stats(request):
"""Get accounting statistics for the current user's organization"""
env = request._run_ns
userorgid = await env.get_userorgid()
now = datetime.now()
today_start = now.strftime('%Y-%m-%d')
month_start = now.strftime('%Y-%m-01')
tomorrow = (now + timedelta(days=1)).strftime('%Y-%m-%d')
stats = {
'total_balance': 0,
'today_amount': 0,
'month_amount': 0,
'account_count': 0
}
async with get_sor_context(request._run_ns, 'accounting') as sor:
# Total balance across all accounts
sql_balance = """
SELECT COALESCE(SUM(CASE WHEN balance_at = '1' THEN balance ELSE -balance END), 0) as total
FROM account
WHERE orgid = ${orgid}$
"""
recs = await sor.sqlExe(sql_balance, {'orgid': userorgid})
if recs:
stats['total_balance'] = float(recs[0].total or 0)
# Account count
sql_count = """
SELECT COUNT(*) as cnt FROM account WHERE orgid = ${orgid}$
"""
recs = await sor.sqlExe(sql_count, {'orgid': userorgid})
if recs:
stats['account_count'] = int(recs[0].cnt or 0)
# Today's consumption (acc_dir=1 means debit/consumption)
sql_today = """
SELECT COALESCE(SUM(amount), 0) as total
FROM acc_detail a
JOIN account b ON a.accountid = b.id
WHERE b.orgid = ${orgid}$
AND a.acc_dir = 1
AND a.acc_date >= ${from_date}$
AND a.acc_date < ${to_date}$
"""
recs = await sor.sqlExe(sql_today, {
'orgid': userorgid,
'from_date': today_start,
'to_date': tomorrow
})
if recs:
stats['today_amount'] = float(recs[0].total or 0)
# This month's consumption
sql_month = """
SELECT COALESCE(SUM(amount), 0) as total
FROM acc_detail a
JOIN account b ON a.accountid = b.id
WHERE b.orgid = ${orgid}$
AND a.acc_dir = 1
AND a.acc_date >= ${from_date}$
AND a.acc_date < ${to_date}$
"""
recs = await sor.sqlExe(sql_month, {
'orgid': userorgid,
'from_date': month_start,
'to_date': tomorrow
})
if recs:
stats['month_amount'] = float(recs[0].total or 0)
return stats

37
models/acc_balance.json Normal file
View File

@ -0,0 +1,37 @@
{
"summary": [
{
"name": "acc_balance",
"title": "账户余额表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "accountid",
"title": "账户id",
"type": "str",
"length": 32
},
{
"name": "acc_date",
"title": "记账日期",
"type": "date"
},
{
"name": "balance",
"title": "账户余额",
"type": "float",
"length": 20,
"dec": 2
}
]
}

72
models/acc_detail.json Normal file
View File

@ -0,0 +1,72 @@
{
"summary": [
{
"name": "acc_detail",
"title": "账户明细表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "accountid",
"title": "账户id",
"type": "str",
"length": 32
},
{
"name": "acc_no",
"title": "明细顺序号",
"type": "short"
},
{
"name": "acc_date",
"title": "记账日期",
"type": "date"
},
{
"name": "acc_timestamp",
"title": "记账时间戳",
"type": "timestamp"
},
{
"name": "acc_dir",
"title": "记账方向",
"type": "str",
"length": 1
},
{
"name": "summary",
"title": "摘要",
"type": "str",
"length": 255
},
{
"name": "amount",
"title": "记账金额",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "balance",
"title": "账户余额",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "acclogid",
"title": "账务流水id",
"type": "str",
"length": 32
}
]
}

100
models/account.json Normal file
View File

@ -0,0 +1,100 @@
{
"summary": [
{
"name": "account",
"title": "机构账户表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "accounting_orgid",
"title": "账本机构",
"type": "str",
"length": 32
},
{
"name": "orgid",
"title": "主机构id",
"type": "str",
"length": 32
},
{
"name": "org1id",
"title": "从机构id",
"type": "str",
"length": 32
},
{
"name": "subjectid",
"title": "科目号",
"type": "str",
"length": 21
},
{
"name": "balance_at",
"title": "余额方向",
"type": "str",
"length": 1
},
{
"name": "max_detailno",
"title": "最大明细顺序号",
"type": "short"
},
{
"name": "balance",
"title": "余额",
"type": "float",
"length": 20,
"dec": 2
}
],
"indexes": [
{
"name": "idx1",
"idxtype": "unique",
"idxfields": [
"accounting_orgid",
"orgid",
"subjectid",
"org1id"
]
}
],
"codes": [
{
"field": "subjectid",
"table": "subject",
"valuefield": "id",
"textfield": "name"
},
{
"field": "orgid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
},
{
"field": "balance_at",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='accounting_dir'"
},
{
"field": "org1id",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
}
]
}

View File

@ -0,0 +1,61 @@
{
"summary": [
{
"name": "account_config",
"title": "账户配置表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "subjectid",
"title": "科目id",
"type": "str",
"length": 32
},
{
"name": "partytype",
"title": "主参与方类型",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "party1type",
"title": "从参与方类型",
"type": "str",
"length": 255,
"nullable": "yes"
}
],
"codes": [
{
"field": "party1type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='partytype'"
},
{
"field": "subjectid",
"table": "subject",
"valuefield": "id",
"textfield": "name"
},
{
"field": "partytype",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='partytype'"
}
]
}

View File

@ -0,0 +1,98 @@
{
"summary": [
{
"name": "accounting_config",
"title": "记账配置表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "action",
"title": "交易",
"type": "str",
"length": 255
},
{
"name": "accounting_orgtype",
"title": "账务机构",
"type": "str",
"length": 256
},
{
"name": "accounting_dir",
"title": "记账方向",
"type": "str",
"length": 255
},
{
"name": "orgtype",
"title": "机构类型",
"type": "str",
"length": 32
},
{
"name": "org1type",
"title": "从机构类型",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "subjectid",
"title": "科目id",
"type": "str",
"length": 21
},
{
"name": "amt_pattern",
"title": "金额模板",
"type": "str",
"length": 255
}
],
"codes": [
{
"field": "accounting_orgtype",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='partytype'"
},
{
"field": "accounting_dir",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='accounting_dir'"
},
{
"field": "orgtype",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='partytype'"
},
{
"field": "org1type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='partytype'"
},
{
"field": "subjectid",
"table": "subject",
"valuefield": "id",
"textfield": "name"
}
]
}

View File

@ -0,0 +1,69 @@
{
"summary": [
{
"name": "accounting_log",
"title": "账务流水表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "accountid",
"title": "账户id",
"type": "str",
"length": 32
},
{
"name": "acc_date",
"title": "记账日期",
"type": "date"
},
{
"name": "acc_timestamp",
"title": "记账时间戳",
"type": "timestamp"
},
{
"name": "acc_dir",
"title": "记账方向",
"type": "str",
"length": 1
},
{
"name": "summary",
"title": "摘要",
"type": "str",
"length": 255
},
{
"name": "amount",
"title": "记账金额",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "billid",
"title": "账单id",
"type": "str",
"length": 32
}
],
"codes": [
{
"field": "acc_dir",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='accounting_dir'"
}
]
}

78
models/bill.json Normal file
View File

@ -0,0 +1,78 @@
{
"summary": [
{
"name": "bill",
"title": "账单",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "customerid",
"title": "客户编号",
"type": "str",
"length": 32
},
{
"name": "resellerid",
"title": "商户id",
"type": "str",
"length": 32
},
{
"name": "productid",
"title": "产品id",
"type": "str",
"length": 32
},
{
"name": "resourceid",
"title": "资源id",
"type": "str",
"length": 32
},
{
"name": "orderid",
"title": "订单编号",
"type": "str",
"length": 32
},
{
"name": "business_op",
"title": "业务操作",
"type": "str",
"length": 255
},
{
"name": "amount",
"title": "金额",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "bill_date",
"title": "账单日期",
"type": "date"
},
{
"name": "bill_timestamp",
"title": "账单时间戳",
"type": "timestamp"
},
{
"name": "bill_state",
"title": "账单状态",
"type": "str",
"length": 1
}
]
}

68
models/bill_detail.json Normal file
View File

@ -0,0 +1,68 @@
{
"summary": [
{
"name": "bill_detail",
"title": "账单明细",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "accounting_orgid",
"title": "账务机构id",
"type": "str",
"length": 32
},
{
"name": "billid",
"title": "账单ID",
"type": "str",
"length": 32
},
{
"name": "description",
"title": "账务说明",
"type": "str",
"length": 255
},
{
"name": "participantid",
"title": "记账方id",
"type": "str",
"length": 32
},
{
"name": "participanttype",
"title": "记账方类型",
"type": "str",
"length": 255
},
{
"name": "subjectname",
"title": "科目名称",
"type": "str",
"length": 255
},
{
"name": "accounting_dir",
"title": "记账方向",
"type": "str",
"length": 255
},
{
"name": "amount",
"title": "账单金额",
"type": "float",
"length": 18,
"dec": 2
}
]
}

50
models/ledger.json Normal file
View File

@ -0,0 +1,50 @@
{
"summary": [
{
"name": "ledger",
"title": "总账表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "accounting_orgid",
"title": "账本机构",
"type": "str",
"length": 32
},
{
"name": "subjectid",
"title": "科目号",
"type": "str",
"length": 32
},
{
"name": "acc_date",
"title": "账务日期",
"type": "date"
},
{
"name": "d_balance",
"title": "借方余额",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "c_balance",
"title": "贷方余额",
"type": "float",
"length": 18,
"dec": 2
}
]
}

53
models/subject.json Normal file
View File

@ -0,0 +1,53 @@
{
"summary": [
{
"name": "subject",
"title": "科目表",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "科目名称",
"type": "str",
"length": 255
},
{
"name": "balance_side",
"title": "余额方向",
"type": "str",
"length": 1
},
{
"name": "subjecttype",
"title": "科目类别",
"type": "str",
"length": 30
}
],
"codes": [
{
"field": "balance_side",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='balance_side'"
},
{
"field": "subjecttype",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='subjecttype'"
}
]
}

99
scripts/load_path.py Normal file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
accounting 模块 RBAC 权限管理脚本
使用方法:
cd ~/repos/sage
./py3/bin/python ~/accounting/scripts/load_path.py
每次代码变更如有新 path 出现需同步更新此脚本
"""
import subprocess
import os
import sys
def find_sage_root():
candidates = [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
]
for c in candidates:
if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
return c
return None
SAGE_ROOT = find_sage_root()
if not SAGE_ROOT:
print("ERROR: Cannot find Sage root directory")
sys.exit(1)
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
MOD = "accounting"
# ============================================================
# 权限路径定义 — 每次新增页面或API时同步更新
# ============================================================
# any — 无需登录(菜单、登录页等)
PATHS_ANY = [
f"/accounting/usermenu.ui",]
# logined — 需要认证的页面和 API
PATHS_LOGINED = [
f"/accounting",
f"/accounting/acc_balance",
f"/accounting/acc_detail",
f"/accounting/accdetail.dspy",
f"/accounting/accdetail.ui",
f"/accounting/account",
f"/accounting/account_config",
f"/accounting/accounting_config",
f"/accounting/accounting_log",
f"/accounting/get_user_balance.dspy",
f"/accounting/index.ui",
f"/accounting/myaccounts.dspy",
f"/accounting/myaccounts.ui",
f"/accounting/mybalance.dspy",
f"/accounting/oca.dspy",
f"/accounting/open_customer_accounts.dspy",
f"/accounting/open_owner_accounts.dspy",
f"/accounting/open_provider_accounts.dspy",
f"/accounting/open_reseller_accounts.dspy",
f"/accounting/open_reseller_provider_accounts.dspy",
f"/accounting/stat_account_count.ui",
f"/accounting/stat_month_consumption.ui",
f"/accounting/stat_today_consumption.ui",
f"/accounting/stat_total_balance.ui",
f"/accounting/subject",]
# ============================================================
# 执行注册
# ============================================================
def run_set_perm(role, path):
cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
def register_role_paths(role, paths):
count = 0
for p in paths:
if run_set_perm(role, p):
count += 1
print(f" {role}: {count}/{len(paths)} paths registered")
return count
def main():
print(f"Sage root: {SAGE_ROOT}")
total = 0
total += register_role_paths("any", PATHS_ANY)
total += register_role_paths("logined", PATHS_LOGINED)
print(f"\nDone. Total {total} permission entries registered.")
print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,53 @@
{% set stats = get_accounting_stats(request) %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\"><path d=\"M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "{{stats.account_count}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "账户数量",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

View File

@ -0,0 +1,53 @@
{% set stats = get_accounting_stats(request) %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#8B5CF6\" stroke-width=\"2\"><path d=\"M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % stats.month_amount}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "本月消费",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

View File

@ -0,0 +1,53 @@
{% set stats = get_accounting_stats(request) %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#F59E0B\" stroke-width=\"2\"><path d=\"M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % stats.today_amount}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "今日消费",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}

View File

@ -0,0 +1,53 @@
{% set stats = get_accounting_stats(request) %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"flex": "1",
"minHeight": "110px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "12px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"2\"><path d=\"M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125H3.375a.75.75 0 01-.75-.75V4.5m0 0V3.75c0-.621.504-1.125 1.125-1.125h1.5c1.243 0 2.25 1.007 2.25 2.25v.375M3.75 4.5h15.75m0 0v-.375c0-.621-.504-1.125-1.125-1.125h-1.5c-1.243 0-2.25 1.007-2.25 2.25v.375M3.75 12.75h15.75M3.75 16.5h15.75\"/></svg>",
"width": "24px",
"height": "24px"
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % stats.total_balance}}",
"fontSize": "32px",
"fontWeight": "700",
"color": "#F1F5F9",
"lineHeight": "1.1"
}
},
{
"widgettype": "Text",
"options": {
"text": "账户总余额",
"fontSize": "14px",
"color": "#94A3B8",
"marginTop": "4px"
}
}
]
}