Compare commits

...

38 Commits

Author SHA1 Message Date
Hermes Agent
f9bc45b97c feat: add i18n translations (zh/en/jp/ko) for all modules 2026-06-19 15:01:15 +08:00
Hermes Agent
656fe2fc51 feat: 新增代客充值和错帐处理页面
- proxy_recharge.ui: Form表单输入客户用户名+充值金额
- proxy_recharge_submit.dspy: 查找用户+创建payment_log+调用recharge_accounting
- error_accounting.ui: 错帐处理管理页面(placeholder,含设计文档)
2026-06-18 13:33:16 +08:00
Hermes Agent
029a76f960 bugfix 2026-06-16 17:32:38 +08:00
Hermes Agent
4a20509490 fix(P0): 修正accounting CRUD JSON格式,添加.gitignore排除自动生成的CRUD目录 2026-06-16 10:45:25 +08:00
1de6fbcf9b chore: remove vim swap file, add .swp to gitignore 2026-06-12 15:08:38 +08:00
65a735764c feat: add credit_status codes to init/data.yaml for credit_limit table 2026-06-12 15:08:25 +08:00
2c6c0570f2 bugfix 2026-06-12 15:06:13 +08:00
414efa7e8f bugfix 2026-06-11 14:02:39 +08:00
72b42cb654 bugfix 2026-06-10 18:28:30 +08:00
7475b30527 bugfix 2026-06-10 17:59:36 +08:00
c637d76a25 fix: replace wildcard patterns with explicit per-file entries in load_path.py 2026-06-04 13:03:27 +08:00
c4a869c9ff fix: billing 500 error (sqlExe returns list not dict) + enlarge popup windows (recharge 600x500, detail 700x550) 2026-05-31 14:36:27 +08:00
434cfe950c feat: billing - add stats summary and Excel download 2026-05-31 14:18:53 +08:00
f62e397c5a chore: remove vim swap file 2026-05-31 14:11:20 +08:00
c141134001 fix: use this.render(params) instead of this.loadData for Tabular (DataViewer API) 2026-05-31 14:11:14 +08:00
dd12be3833 bugix 2026-05-31 14:08:52 +08:00
7fbc826330 bugix 2026-05-31 14:06:32 +08:00
1029078b56 bugix 2026-05-31 14:05:05 +08:00
0080bbd7c4 fix: IconBar click not working - add id and reference in binds instead of self 2026-05-31 14:00:08 +08:00
8d447b90ea refactor: billing - use Form title, Tabular always present, form submit loadData via script 2026-05-31 13:56:56 +08:00
6d3dcf2db9 fix: billing layout - wrap Form in fixed-height VBox, Tabular as filler 2026-05-31 13:46:33 +08:00
eec08d684c bugfix 2026-05-31 13:42:07 +08:00
7c2c584407 fix: billing layout - Form fixed height, Tabular css filler 2026-05-31 13:30:32 +08:00
7361614f89 fix: billing query - use Tabular instead of DataViewer, fix form submit target
- Changed DataViewer to Tabular with row_options.fields for proper table rendering
- Form submit target changed from 'self' to 'billing_page' (root VBox ID)
  with mode:replace, preventing nested page loading inside Form
- Fixed page_size to page_rows (correct parameter name)
- Added column definitions: date, time, subject, direction, summary, amount, balance
2026-05-31 13:24:32 +08:00
1fddb96291 fix: myaccounts.ui icon click - fix event name, entire_url syntax, paths 2026-05-31 11:48:24 +08:00
8a3f1955d3 fix: collation mismatch in SQL JOINs + billing date fields use uitype:date 2026-05-31 11:44:58 +08:00
997c7a445e feat: 新增账单查询页面,按机构id+日期范围查询acc_detail 2026-05-31 10:27:14 +08:00
78ff190789 feat(credit): redesign credit limit UI with dashboard, overview and management views
- Add hub.ui as main entry with stat cards (total/used/available/usage%)
- Add credit_overview.ui for user's own credit visualization with progress bars
- Add credit_manage.ui for distributor sales to manage customer credits
- Add set_credit_form.ui and set_customer_credit.dspy for credit adjustment
- Add credit_summary.dspy API for stats data
- Enhance creditlimit.py with get_credit_stats, get_my_credit_list, get_all_customer_credits
- Register new functions in init.py with ServerEnv
2026-05-30 21:00:27 +08:00
195a7bfb46 fix: wrap Tabular in VBox with cheight for proper scrolling 2026-05-29 22:09:45 +08:00
8f36013ad6 refactor: use wildcard % in load_path.py for auto-coverage 2026-05-29 00:52:18 +08:00
5fa058add9 fix: correct indentation in leg_accounting credit limit block
Two bugs fixed:
1. FATAL: lines 213-278 were indented inside the 'if new_balance < 0' block,
   causing all normal (positive balance) accounting operations to be skipped.
   All post-credit-check code now correctly at method body level (2 tabs).

2. LOGIC: added else clause to reset used_credit to 0 when balance returns
   to non-negative (e.g. after recharge). Previously used_credit stayed
   stale after account recovered from overdraft.
2026-05-28 23:05:23 +08:00
5da6ddd7d5 feat: expose credit limit functions via ServerEnv and register RBAC paths 2026-05-28 22:39:48 +08:00
9696d4334b feat: 添加信用额度功能
- 新增 credit_limit 表定义和DDL
- 修改 accounting_config.py 支持信用额度透支检查
- 新增 creditlimit.py 信用额度管理模块
- 新增信用额度管理界面和CRUD API
- 支持设置/查询/更新客户信用额度
2026-05-28 19:09:04 +08:00
e34be6ad16 Merge branch 'main' of git.opencomputing.cn:yumoqing/accounting
# Conflicts:
#	wwwroot/index.ui
#	wwwroot/index.ui~be97eaf7b5edfa4867c9caa6a5e0ffd1e499faa3
2026-05-28 19:08:47 +08:00
be97eaf7b5 fix: remove hardcoded dark theme colors from index.ui 2026-05-28 16:14:57 +08:00
a633368dcb fix: remove hardcoded dark theme colors from index.ui 2026-05-28 16:14:03 +08:00
af10e4a810 refactor(models): convert to json format per database-table-definition-spec 2026-05-27 13:23:22 +08:00
cb52542567 feat: add load_path.py RBAC permission registration script 2026-05-27 13:16:01 +08:00
48 changed files with 4277 additions and 553 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*.swp
# CRUD auto-generated directories
wwwroot/account/
wwwroot/subject/
wwwroot/acc_balance/
wwwroot/acc_detail/

View File

@ -1,272 +1,282 @@
import asyncio
from traceback import format_exc
import re
from operator import itemgetter
from .const import *
from .accountingnode import get_parent_orgid
from .excep import *
from .getaccount import get_account, getAccountByName
from appPublic.uniqueID import getID
from appPublic.log import debug, exception
from sqlor.dbpools import DBPools
from appPublic.timeUtils import curDateString
# from .argsconvert import ArgsConvert
from appPublic.argsConvert import ArgsConvert
from datetime import datetime
accounting_config = None
class PFBiz:
async def get_orgid_by_trans_role(self, sor, leg, role):
pass
async def get_accounting_config(sor):
global accounting_config
if accounting_config:
return accounting_config
recs = await sor.R('accounting_config', {})
if len(recs) > 0:
accounting_config = recs
return accounting_config
return None
class Accounting:
"""
需要caller功能
caller中要有分录中的变量
get_accounting_orgid(leg) 获得记账机构
get_account(legaccounting_orgid) 获得记账账号通过科目机构类型账务机构确定一个唯一的账号
"""
def __init__(self, caller):
debug(f'caller={caller}')
if isinstance(caller, list):
self.callers = caller
caller = self.callers[0]
else:
self.callers = [caller]
self.caller = caller
async def setup_all_accounting_legs(self):
self.accounting_legs = []
debug(f'{self.callers=}')
for i, caller in enumerate(self.callers):
self.caller = caller
self.curdate = caller.curdate
self.realtimesettled = False
self.timestamp = caller.timestamp
self.billid = caller.billid
self.action = caller.action
self.summary = f'{self.caller.orderid}:{self.caller.billid}'
self.providerid = caller.providerid
self.productid = caller.productid
self.resellerid = caller.resellerid
self.customerid = caller.customerid
self.own_salemode = None
self.reseller_salemode = None
self.variable = caller.variable
await self.setup_accounting_legs(i)
try:
legs = sorted(
self.accounting_legs,
key=lambda x: (
x.get('accounting_orgid','0'),
x.get('orgid', ''),
x.get('subjectid', ''),
0 if x.get('acc_dir', '0') == x.get('balance_at', '0') else 1
)
)
self.accounting_legs = legs
except Exception as e:
exception(f'{self.accounting_legs=}, {e=}\n{format_exc()}')
await self.get_legs_account()
async def setup_accounting_legs(self, pos):
sor = self.sor
action = self.action.split('_')[0]
acfg = await get_accounting_config(self.sor)
legs = [r.copy() for r in acfg
if r.action == action ]
debug(f'{legs=}')
rev = self.action.endswith('_REVERSE')
for l in legs:
l['position'] = pos
if rev:
l['acc_dir'] = DEBT if l['accounting_dir'] == CREDIT else CREDIT
else:
l['acc_dir'] = l['accounting_dir']
ac = ArgsConvert('${', '}$')
try:
amtstr = ac.convert(l['amt_pattern'],
self.variable.copy()
)
debug(f'{l["amt_pattern"]=}, {amtstr=}, {self.variable=}')
if isinstance(amtstr, str):
l['amount'] = eval(amtstr)
else:
l['amount'] = amtstr
except Exception as e:
exception(f"{e=}, {l['amt_pattern']}, {self.variable=}")
raise e
if l['amount'] is None:
debug(f'amount is None:{l["amt_pattern"]}, {self.variable=},{self.caller.billid=}')
raise AccountingAmountIsNone(self.caller.billid)
accounting_orgid = await self.caller.get_orgid_by_trans_role(sor, l, l.accounting_orgtype)
orgid = await self.caller.get_orgid_by_trans_role(sor, l, l.orgtype)
org1id = None if l.org1type is None else \
await self.caller.get_orgid_by_trans_role(sor, l, l.org1type)
l['accounting_orgid'] = accounting_orgid
l['orgid'] = orgid
l['org1id'] = org1id
self.accounting_legs += legs
async def get_legs_account(self):
sor = self.sor
oldk = ''
acc = None
for l in self.accounting_legs:
k = f'{l.accounting_orgid}|{l.orgid}|{l.subjectid}|{l.org1id}'
if oldk != k:
acc = await get_account(sor, l.accounting_orgid, l.orgid,
l.subjectid, org1id=l.org1id, update=True)
if acc is None:
debug(f'can not get accountid {l.accounting_orgid=}, {l.orgid=},{l.subjectid=}, {l.org1id=}, {self.customerid=},{self.resellerid=},{self.providerid=}')
raise AccountIdNone(l.accounting_orgid, l.orgid, l.subjectid)
oldk = k
l['accid'] = acc.id
l['balance_at'] = acc.balance_at
l['acc'] = acc
def check_accounting_balance(self, legs):
debt_balance = 0.0
credit_balance = 0.0
for l in legs:
if l['acc_dir'] != l['balance_at']:
l['balance_amount'] = -l['amount']
else:
l['balance_amount'] = l['amount']
if l['acc_dir'] == DEBT:
debt_balance += l['amount']
else:
credit_balance += l['amount']
if abs(credit_balance - debt_balance) >= 0.00001:
e = Exception('accounting legs not balance')
exception(f'{legs=}, {e=}')
raise e
async def do_accounting(self, sor):
self.sor = sor
await self.setup_all_accounting_legs()
# debug(f'do_accounting() ...{self.accounting_legs=}')
legs = [ l for l in self.accounting_legs if l['amount'] > 0.0001 ]
self.accounting_legs = legs
self.check_accounting_balance(self.accounting_legs)
for leg in self.accounting_legs:
await self.leg_accounting(sor, leg)
async def write_settle_log(self):
sale_mode = {
SALEMODE_DISCOUNT:'0',
SALEMODE_REBATE:'1',
SALEMODE_FLOORPRICE:'2'
}
ns = {
'id':getID(),
'accounting_orgid':self.accounting_orgid,
'providerid':self.providerid,
'sale_mode':sale_mode.get(self.own_salemode),
'settle_date':self.curdate,
'settle_amt':self.accounting_legs[-1]['amount']
}
sor = self.sor
await sor.C('settle_log', ns)
async def leg_accounting(self, sor, leg):
# print(f'leg_accounting(), {leg=}')
if leg['amount'] < 0.00001:
return
accid = leg['accid']
sql = "select * from account where id=${accid}$ for update"
accounts = await sor.sqlExe(sql, {'accid': accid})
if len(accounts) == 0:
e = Exception(f'{accid} account not exist')
exception(f'{e}')
raise e
account = accounts[0]
new_balance = account.balance + leg['balance_amount']
if new_balance < -0.0000001:
e = AccountOverDraw(accid, account.balance, leg['amount'])
exception(f'{e},{leg=}')
raise e
subjects = await sor.R('subject', {'id': leg['subjectid']})
if len(subjects) > 0:
leg['subjectname'] = subjects[0].name
# write acc_balance
sql = """select * from acc_balance
where accountid=${accid}$
and acc_date = ${curdate}$ for update"""
recs = await sor.sqlExe(sql, {'accid':accid, 'curdate':self.curdate})
if len(recs) == 0:
ns = {
'id':getID(),
'accountid':accid,
'acc_date':self.curdate,
'balance': new_balance
}
await sor.C('acc_balance', ns.copy())
else:
ns = recs[0]
ns['balance'] = new_balance
await sor.U('acc_balance', ns.copy())
# summary = self.summary
ns = {
'id':getID(),
'accounting_orgid' : leg['accounting_orgid'],
'billid' : self.billid,
'description' : self.summary,
'participantid' : leg['orgid'],
'participant1id' : leg['org1id'],
'participanttype' : leg['orgtype'],
'participant1type' : leg['org1type'],
'subjectname' : leg['subjectname'],
'accounting_dir': leg['accounting_dir'],
'amount' : leg['amount']
}
await sor.C('bill_detail', ns)
logid = getID()
ns = {
'id':logid,
'accountid':accid,
'acc_date':self.curdate,
'acc_timestamp':self.timestamp,
'acc_dir':leg['acc_dir'],
'summary':self.summary,
'amount':leg['amount'],
'billid':self.billid
}
await sor.C('accounting_log', ns.copy())
ns = {
'id':getID(),
'accountid':accid,
'acc_no': account.max_detailno + 1,
'acc_date':self.curdate,
'acc_timestamp':self.timestamp,
'acc_dir':leg['acc_dir'],
'summary':self.summary,
'amount':leg['amount'],
'balance': new_balance,
'acclogid':logid
}
await sor.C('acc_detail', ns.copy())
await sor.U('account', {
'id': accid,
'max_detailno': account.max_detailno + 1,
'balance': new_balance
})
import asyncio
from traceback import format_exc
import re
from operator import itemgetter
from .const import *
from .accountingnode import get_parent_orgid
from .excep import *
from .getaccount import get_account, getAccountByName
from appPublic.uniqueID import getID
from appPublic.log import debug, exception
from sqlor.dbpools import DBPools
from appPublic.timeUtils import curDateString
# from .argsconvert import ArgsConvert
from appPublic.argsConvert import ArgsConvert
from datetime import datetime
from .creditlimit import get_credit_limit_for_account, update_used_credit
accounting_config = None
class PFBiz:
async def get_orgid_by_trans_role(self, sor, leg, role):
pass
async def get_accounting_config(sor):
global accounting_config
if accounting_config:
return accounting_config
recs = await sor.R('accounting_config', {})
if len(recs) > 0:
accounting_config = recs
return accounting_config
return None
class Accounting:
"""
需要caller功能
caller中要有分录中的变量
get_accounting_orgid(leg) 获得记账机构
get_account(legaccounting_orgid) 获得记账账号通过科目机构类型账务机构确定一个唯一的账号
"""
def __init__(self, caller):
debug(f'caller={caller}')
if isinstance(caller, list):
self.callers = caller
caller = self.callers[0]
else:
self.callers = [caller]
self.caller = caller
async def setup_all_accounting_legs(self):
self.accounting_legs = []
debug(f'{self.callers=}')
for i, caller in enumerate(self.callers):
self.caller = caller
self.curdate = caller.curdate
self.realtimesettled = False
self.timestamp = caller.timestamp
self.billid = caller.billid
self.action = caller.action
self.summary = f'{self.caller.orderid}:{self.caller.billid}'
self.providerid = caller.providerid
self.productid = caller.productid
self.resellerid = caller.resellerid
self.customerid = caller.customerid
self.own_salemode = None
self.reseller_salemode = None
self.variable = caller.variable
await self.setup_accounting_legs(i)
try:
legs = sorted(
self.accounting_legs,
key=lambda x: (
x.get('accounting_orgid','0'),
x.get('orgid', ''),
x.get('subjectid', ''),
0 if x.get('acc_dir', '0') == x.get('balance_at', '0') else 1
)
)
self.accounting_legs = legs
except Exception as e:
exception(f'{self.accounting_legs=}, {e=}\n{format_exc()}')
await self.get_legs_account()
async def setup_accounting_legs(self, pos):
sor = self.sor
action = self.action.split('_')[0]
acfg = await get_accounting_config(self.sor)
legs = [r.copy() for r in acfg
if r.action == action ]
debug(f'{legs=}')
rev = self.action.endswith('_REVERSE')
for l in legs:
l['position'] = pos
if rev:
l['acc_dir'] = DEBT if l['accounting_dir'] == CREDIT else CREDIT
else:
l['acc_dir'] = l['accounting_dir']
ac = ArgsConvert('${', '}$')
try:
amtstr = ac.convert(l['amt_pattern'],
self.variable.copy()
)
debug(f'{l["amt_pattern"]=}, {amtstr=}, {self.variable=}')
if isinstance(amtstr, str):
l['amount'] = eval(amtstr)
else:
l['amount'] = amtstr
except Exception as e:
exception(f"{e=}, {l['amt_pattern']}, {self.variable=}")
raise e
if l['amount'] is None:
debug(f'amount is None:{l["amt_pattern"]}, {self.variable=},{self.caller.billid=}')
raise AccountingAmountIsNone(self.caller.billid)
accounting_orgid = await self.caller.get_orgid_by_trans_role(sor, l, l.accounting_orgtype)
orgid = await self.caller.get_orgid_by_trans_role(sor, l, l.orgtype)
org1id = None if l.org1type is None else \
await self.caller.get_orgid_by_trans_role(sor, l, l.org1type)
l['accounting_orgid'] = accounting_orgid
l['orgid'] = orgid
l['org1id'] = org1id
self.accounting_legs += legs
async def get_legs_account(self):
sor = self.sor
oldk = ''
acc = None
for l in self.accounting_legs:
k = f'{l.accounting_orgid}|{l.orgid}|{l.subjectid}|{l.org1id}'
if oldk != k:
acc = await get_account(sor, l.accounting_orgid, l.orgid,
l.subjectid, org1id=l.org1id, update=True)
if acc is None:
debug(f'can not get accountid {l.accounting_orgid=}, {l.orgid=},{l.subjectid=}, {l.org1id=}, {self.customerid=},{self.resellerid=},{self.providerid=}')
raise AccountIdNone(l.accounting_orgid, l.orgid, l.subjectid)
oldk = k
l['accid'] = acc.id
l['balance_at'] = acc.balance_at
l['acc'] = acc
def check_accounting_balance(self, legs):
debt_balance = 0.0
credit_balance = 0.0
for l in legs:
if l['acc_dir'] != l['balance_at']:
l['balance_amount'] = -l['amount']
else:
l['balance_amount'] = l['amount']
if l['acc_dir'] == DEBT:
debt_balance += l['amount']
else:
credit_balance += l['amount']
if abs(credit_balance - debt_balance) >= 0.00001:
e = Exception('accounting legs not balance')
exception(f'{legs=}, {e=}')
raise e
async def do_accounting(self, sor):
self.sor = sor
await self.setup_all_accounting_legs()
# debug(f'do_accounting() ...{self.accounting_legs=}')
legs = [ l for l in self.accounting_legs if l['amount'] > 0.0001 ]
self.accounting_legs = legs
self.check_accounting_balance(self.accounting_legs)
for leg in self.accounting_legs:
await self.leg_accounting(sor, leg)
async def write_settle_log(self):
sale_mode = {
SALEMODE_DISCOUNT:'0',
SALEMODE_REBATE:'1',
SALEMODE_FLOORPRICE:'2'
}
ns = {
'id':getID(),
'accounting_orgid':self.accounting_orgid,
'providerid':self.providerid,
'sale_mode':sale_mode.get(self.own_salemode),
'settle_date':self.curdate,
'settle_amt':self.accounting_legs[-1]['amount']
}
sor = self.sor
await sor.C('settle_log', ns)
async def leg_accounting(self, sor, leg):
# print(f'leg_accounting(), {leg=}')
if leg['amount'] < 0.00001:
return
accid = leg['accid']
sql = "select * from account where id=${accid}$ for update"
accounts = await sor.sqlExe(sql, {'accid': accid})
if len(accounts) == 0:
e = Exception(f'{accid} account not exist')
exception(f'{e}')
raise e
account = accounts[0]
new_balance = account.balance + leg['balance_amount']
# Check credit limit if balance goes negative
if new_balance < -0.0000001:
credit_limit = await get_credit_limit_for_account(sor, accid)
if credit_limit is None or credit_limit['available_credit'] < abs(new_balance):
e = AccountOverDraw(accid, account.balance, leg['amount'])
exception(f'{e},{leg=}')
raise e
# Update used credit
await update_used_credit(sor, accid, abs(new_balance))
else:
# Balance is non-negative, reset used credit if any
await update_used_credit(sor, accid, 0)
subjects = await sor.R('subject', {'id': leg['subjectid']})
if len(subjects) > 0:
leg['subjectname'] = subjects[0].name
# write acc_balance
sql = """select * from acc_balance
where accountid=${accid}$
and acc_date = ${curdate}$ for update"""
recs = await sor.sqlExe(sql, {'accid':accid, 'curdate':self.curdate})
if len(recs) == 0:
ns = {
'id':getID(),
'accountid':accid,
'acc_date':self.curdate,
'balance': new_balance
}
await sor.C('acc_balance', ns.copy())
else:
ns = recs[0]
ns['balance'] = new_balance
await sor.U('acc_balance', ns.copy())
# summary = self.summary
ns = {
'id':getID(),
'accounting_orgid' : leg['accounting_orgid'],
'billid' : self.billid,
'description' : self.summary,
'participantid' : leg['orgid'],
'participant1id' : leg['org1id'],
'participanttype' : leg['orgtype'],
'participant1type' : leg['org1type'],
'subjectname' : leg['subjectname'],
'accounting_dir': leg['accounting_dir'],
'amount' : leg['amount']
}
await sor.C('bill_detail', ns)
logid = getID()
ns = {
'id':logid,
'accountid':accid,
'acc_date':self.curdate,
'acc_timestamp':self.timestamp,
'acc_dir':leg['acc_dir'],
'summary':self.summary,
'amount':leg['amount'],
'billid':self.billid
}
await sor.C('accounting_log', ns.copy())
ns = {
'id':getID(),
'accountid':accid,
'acc_no': account.max_detailno + 1,
'acc_date':self.curdate,
'acc_timestamp':self.timestamp,
'acc_dir':leg['acc_dir'],
'summary':self.summary,
'amount':leg['amount'],
'balance': new_balance,
'acclogid':logid
}
await sor.C('acc_detail', ns.copy())
await sor.U('account', {
'id': accid,
'max_detailno': account.max_detailno + 1,
'balance': new_balance
})

196
accounting/creditlimit.py Normal file
View File

@ -0,0 +1,196 @@
from appPublic.log import debug, exception
from appPublic.uniqueID import getID
from datetime import datetime
async def get_credit_stats(sor, orgid):
"""
Get credit summary statistics for an organization.
Returns total_credit, total_used, total_available, usage_pct, customer_count.
"""
sql = """
SELECT
COALESCE(SUM(credit_limit), 0) as total_credit,
COALESCE(SUM(used_credit), 0) as total_used,
COALESCE(SUM(available_credit), 0) as total_available,
COUNT(*) as customer_count,
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_count,
COUNT(CASE WHEN status = 'expired' THEN 1 END) as expired_count
FROM credit_limit
WHERE orgid = ${orgid}$
"""
recs = await sor.sqlExe(sql, {'orgid': orgid})
if recs and len(recs) > 0:
r = recs[0]
total_credit = float(r.total_credit or 0)
total_used = float(r.total_used or 0)
total_available = float(r.total_available or 0)
usage_pct = round((total_used / total_credit * 100), 1) if total_credit > 0 else 0
return {
'total_credit': total_credit,
'total_used': total_used,
'total_available': total_available,
'usage_pct': usage_pct,
'customer_count': int(r.customer_count or 0),
'active_count': int(r.active_count or 0),
'expired_count': int(r.expired_count or 0)
}
return {
'total_credit': 0, 'total_used': 0, 'total_available': 0,
'usage_pct': 0, 'customer_count': 0, 'active_count': 0, 'expired_count': 0
}
async def get_my_credit_list(sor, orgid):
"""
Get all credit limit records for the current user's organization,
with organization name and account info for display.
"""
sql = """
SELECT
cl.*,
org.orgname as orgname_text,
sub.name as subject_name,
CASE
WHEN cl.credit_limit > 0 THEN ROUND(cl.used_credit / cl.credit_limit * 100, 1)
ELSE 0
END as usage_pct
FROM credit_limit cl
LEFT JOIN organization org ON cl.orgid = org.id COLLATE utf8mb4_unicode_ci
LEFT JOIN account acc ON cl.accountid = acc.id COLLATE utf8mb4_unicode_ci
LEFT JOIN subject sub ON acc.subjectid = sub.id COLLATE utf8mb4_unicode_ci
WHERE cl.orgid = ${orgid}$
ORDER BY cl.created_at DESC
"""
recs = await sor.sqlExe(sql, {'orgid': orgid})
return recs
async def get_all_customer_credits(sor, orgid, status_filter=None):
"""
Get all customer credit limits for management view.
For distributor sales to see all their customers' credit status.
"""
where_clause = "WHERE cl.orgid = ${orgid}$"
params = {'orgid': orgid}
if status_filter and status_filter != 'all':
where_clause += " AND cl.status = ${status}$"
params['status'] = status_filter
sql = f"""
SELECT
cl.*,
org.orgname as orgname_text,
sub.name as subject_name,
acc.balance as account_balance,
CASE
WHEN cl.credit_limit > 0 THEN ROUND(cl.used_credit / cl.credit_limit * 100, 1)
ELSE 0
END as usage_pct
FROM credit_limit cl
LEFT JOIN organization org ON cl.orgid = org.id COLLATE utf8mb4_unicode_ci
LEFT JOIN account acc ON cl.accountid = acc.id COLLATE utf8mb4_unicode_ci
LEFT JOIN subject sub ON acc.subjectid = sub.id COLLATE utf8mb4_unicode_ci
{where_clause}
ORDER BY cl.updated_at DESC
"""
recs = await sor.sqlExe(sql, params)
return recs
async def get_credit_limit_for_account(sor, accid):
"""
Get active credit limit for an account.
Returns credit_limit record if active and valid, None otherwise.
"""
sql = """
SELECT * FROM credit_limit
WHERE accountid = ${accid}$
AND status = 'active'
AND (valid_from IS NULL OR valid_from <= CURRENT_DATE)
AND (valid_to IS NULL OR valid_to >= CURRENT_DATE)
ORDER BY created_at DESC
LIMIT 1
"""
recs = await sor.sqlExe(sql, {'accid': accid})
if len(recs) == 0:
return None
return recs[0]
async def update_used_credit(sor, accid, new_used_amount):
"""
Update used_credit and available_credit for an account.
new_used_amount is the absolute value of negative balance.
"""
credit = await get_credit_limit_for_account(sor, accid)
if credit is None:
return
new_used = new_used_amount
new_available = credit['credit_limit'] - new_used
sql = """
UPDATE credit_limit
SET used_credit = ${used}$,
available_credit = ${available}$,
updated_at = CURRENT_TIMESTAMP
WHERE id = ${id}$
"""
await sor.sqlExe(sql, {
'used': new_used,
'available': new_available,
'id': credit['id']
})
debug(f'Updated credit for {accid}: used={new_used}, available={new_available}')
async def set_credit_limit(sor, accountid, orgid, credit_limit_amount,
valid_from=None, valid_to=None, created_by=None, remark=None):
"""
Set or update credit limit for an account.
If a credit limit already exists, update it; otherwise create new.
"""
# Check if credit limit exists
existing = await get_credit_limit_for_account(sor, accountid)
if existing:
# Update existing
sql = """
UPDATE credit_limit
SET credit_limit = ${credit_limit}$,
available_credit = ${credit_limit}$ - used_credit,
valid_from = ${valid_from}$,
valid_to = ${valid_to}$,
remark = ${remark}$,
updated_at = CURRENT_TIMESTAMP
WHERE id = ${id}$
"""
await sor.sqlExe(sql, {
'credit_limit': credit_limit_amount,
'valid_from': valid_from,
'valid_to': valid_to,
'remark': remark,
'id': existing['id']
})
debug(f'Updated credit limit for {accountid}: {credit_limit_amount}')
return existing['id']
else:
# Create new
new_id = getID()
ns = {
'id': new_id,
'accountid': accountid,
'orgid': orgid,
'credit_limit': credit_limit_amount,
'used_credit': 0,
'available_credit': credit_limit_amount,
'valid_from': valid_from,
'valid_to': valid_to,
'status': 'active',
'created_at': datetime.now(),
'updated_at': datetime.now(),
'created_by': created_by,
'remark': remark
}
await sor.C('credit_limit', ns)
debug(f'Created credit limit for {accountid}: {credit_limit_amount}')
return new_id

View File

@ -10,6 +10,7 @@ from .getaccount import getAccountBalance, getCustomerBalance, getAccountByName,
from .stats import get_accounting_stats
from .recharge import RechargeBiz, recharge_accounting
from .consume import consume_accounting
from .creditlimit import get_credit_limit_for_account, update_used_credit, set_credit_limit, get_credit_stats, get_my_credit_list, get_all_customer_credits
async def all_my_accounts(request):
env = request._run_ns
@ -73,3 +74,37 @@ def load_accounting():
g.all_my_accounts = all_my_accounts
g.openRetailRelationshipAccounts = openRetailRelationshipAccounts
g.get_accounting_stats = get_accounting_stats
g.get_credit_limit_for_account = get_credit_limit_for_account
g.update_used_credit = update_used_credit
g.set_credit_limit = set_credit_limit
g.get_credit_stats = get_credit_stats
g.get_my_credit_list = get_my_credit_list
g.get_all_customer_credits = get_all_customer_credits
g.get_credit_stats_web = get_credit_stats_web
g.get_my_credits_web = get_my_credits_web
g.get_all_credits_web = get_all_credits_web
async def get_credit_stats_web(request):
"""Web wrapper for get_credit_stats - used in Jinja2 .ui templates"""
env = request._run_ns
userorgid = await env.get_userorgid()
async with get_sor_context(env, 'accounting') as sor:
return await get_credit_stats(sor, userorgid)
async def get_my_credits_web(request):
"""Web wrapper for get_my_credit_list - used in Jinja2 .ui templates"""
env = request._run_ns
userorgid = await env.get_userorgid()
async with get_sor_context(env, 'accounting') as sor:
return await get_my_credit_list(sor, userorgid)
async def get_all_credits_web(request):
"""Web wrapper for get_all_customer_credits - used in Jinja2 .ui templates"""
env = request._run_ns
userorgid = await env.get_userorgid()
status_filter = getattr(request, '_params_kw', {}).get('status', None) if hasattr(request, '_params_kw') else None
async with get_sor_context(env, 'accounting') as sor:
return await get_all_customer_credits(sor, userorgid, status_filter)

View File

@ -42,7 +42,7 @@ async def get_accounting_stats(request):
sql_today = """
SELECT COALESCE(SUM(amount), 0) as total
FROM acc_detail a
JOIN account b ON a.accountid = b.id
JOIN account b ON a.accountid = b.id COLLATE utf8mb4_unicode_ci
WHERE b.orgid = ${orgid}$
AND a.acc_dir = 1
AND a.acc_date >= ${from_date}$
@ -60,7 +60,7 @@ async def get_accounting_stats(request):
sql_month = """
SELECT COALESCE(SUM(amount), 0) as total
FROM acc_detail a
JOIN account b ON a.accountid = b.id
JOIN account b ON a.accountid = b.id COLLATE utf8mb4_unicode_ci
WHERE b.orgid = ${orgid}$
AND a.acc_dir = 1
AND a.acc_date >= ${from_date}$

174
i18n/en/msg.txt Normal file
View File

@ -0,0 +1,174 @@
余额: Balance
明细: Details
科目: Account
账户: Account
会计科目管理: Chart of Accounts Management
科目编码: Account Code
科目名称: Account Name
科目类型: Account Type
上级科目: Parent Account
科目级别: Account Level
辅助核算: Auxiliary Accounting
状态: Status
启用: Enable
停用: Disable
新增科目: Add Account
编辑科目: Edit Account
删除科目: Delete Account
搜索: Search
科目编码或名称: Account Code or Name
资产: Assets
负债: Liabilities
所有者权益: Owner's Equity
成本: Cost
损益: Profit & Loss
借方: Debit
贷方: Credit
方向: Direction
期初余额: Opening Balance
期末余额: Closing Balance
本期借方: Current Period Debit
本期贷方: Current Period Credit
累计借方: Cumulative Debit
累计贷方: Cumulative Credit
凭证日期: Voucher Date
凭证号: Voucher No.
摘要: Summary
金额: Amount
合计: Total
制单人: Prepared By
审核人: Reviewed By
记账人: Posted By
未记账: Unposted
已记账: Posted
已审核: Approved
未审核: Unapproved
新增凭证: Add Voucher
编辑凭证: Edit Voucher
删除凭证: Delete Voucher
审核凭证: Review Voucher
记账: Post
反记账: Unpost
凭证类型: Voucher Type
收款凭证: Receipt Voucher
付款凭证: Payment Voucher
转账凭证: Transfer Voucher
记账凭证: Journal Voucher
附件数: Attachments
总账: General Ledger
明细账: Subsidiary Ledger
科目余额表: Trial Balance of Accounts
资产负债表: Balance Sheet
利润表: Income Statement
现金流量表: Cash Flow Statement
试算平衡: Trial Balance
会计期间: Accounting Period
年度: Year
月份: Month
期初: Period Start
期末: Period End
本年累计: YTD
本月合计: Monthly Total
过账: Posting
结账: Closing
反结账: Unclosing
凭证查询: Voucher Query
科目查询: Account Query
辅助核算项目: Auxiliary Accounting Item
客户: Customer
供应商: Supplier
部门: Department
项目: Project
员工: Employee
核算类别: Accounting Category
新增: Add
保存: Save
取消: Cancel
确认: Confirm
删除: Delete
编辑: Edit
查看: View
导出: Export
打印: Print
刷新: Refresh
返回: Back
提交: Submit
重置: Reset
Conform: Conform
Discard: Discard
Submit: Submit
Reset: Reset
Cancel: Cancel
凭证: Voucher
总分类账: General Ledger
核算项目余额: Accounting Item Balance
科目汇总表: Account Summary
数量: Quantity
单价: Unit Price
外币: Foreign Currency
汇率: Exchange Rate
原币: Original Currency
本位币: Local Currency
辅助账: Auxiliary Ledger
日记账: Journal
多栏账: Multi-column Ledger
核算项目明细账: Accounting Item Detail Ledger
数量金额明细账: Quantity-Amount Detail Ledger
数量金额总账: Quantity-Amount General Ledger
固定资产: Fixed Assets
工资: Salary
往来: Current Account
自定义辅助核算: Custom Auxiliary Accounting
核算项目: Accounting Item
核算类别名称: Accounting Category Name
辅助核算编码: Auxiliary Accounting Code
辅助核算名称: Auxiliary Accounting Name
余额方向: Balance Direction
余额方向(借/贷): Balance Direction (Debit/Credit)
借方发生额: Debit Amount
贷方发生额: Credit Amount
借方累计: Cumulative Debit
贷方累计: Cumulative Credit
年初余额: Year Opening Balance
年累计借方: YTD Debit
年累计贷方: YTD Credit
年累计余额: YTD Balance
本月借方发生额: Monthly Debit Amount
本月贷方发生额: Monthly Credit Amount
本年借方发生额: Annual Debit Amount
本年贷方发生额: Annual Credit Amount
年初借方余额: Year Opening Debit Balance
年初贷方余额: Year Opening Credit Balance
期初借方余额: Opening Debit Balance
期初贷方余额: Opening Credit Balance
期末借方余额: Closing Debit Balance
期末贷方余额: Closing Credit Balance
损益结转: P&L Carry-forward
结转损益: Carry Forward P&L
凭证字号: Voucher Prefix No.
凭证字: Voucher Prefix
记账日期: Posting Date
制单日期: Preparation Date
记账状态: Posting Status
审核状态: Review Status
作废: Void
恢复: Restore
冲销: Reverse
红冲: Red Reverse
反审核: Unapprove
全部: All
已作废: Voided
已冲销: Reversed
操作: Action
备注: Remarks
日期: Date
描述: Description
类型: Type
名称: Name
编码: Code
编号: Number
金额(借方): Amount (Debit)
金额(贷方): Amount (Credit)
金额方向: Amount Direction
记账金额: Posting Amount
凭证编号: Voucher Number

174
i18n/jp/msg.txt Normal file
View File

@ -0,0 +1,174 @@
余额: 残高
明细: 明細
科目: 科目
账户: 口座
会计科目管理: 勘定科目管理
科目编码: 科目コード
科目名称: 科目名
科目类型: 科目タイプ
上级科目: 上位科目
科目级别: 科目レベル
辅助核算: 補助核算
状态: ステータス
启用: 有効化
停用: 無効化
新增科目: 科目追加
编辑科目: 科目編集
删除科目: 科目削除
搜索: 検索
科目编码或名称: 科目コードまたは名称
资产: 資産
负债: 負債
所有者权益: 純資産
成本: コスト
损益: 損益
借方: 借方
贷方: 貸方
方向: 方向
期初余额: 期首残高
期末余额: 期末残高
本期借方: 当期借方
本期贷方: 当期貸方
累计借方: 累計借方
累计贷方: 累計貸方
凭证日期: 伝票日付
凭证号: 伝票番号
摘要: 摘要
金额: 金額
合计: 合計
制单人: 作成者
审核人: 承認者
记账人: 記帳者
未记账: 未記帳
已记账: 記帳済み
已审核: 承認済み
未审核: 未承認
新增凭证: 伝票追加
编辑凭证: 伝票編集
删除凭证: 伝票削除
审核凭证: 伝票承認
记账: 記帳
反记账: 記帳取消
凭证类型: 伝票タイプ
收款凭证: 入金伝票
付款凭证: 出金伝票
转账凭证: 振替伝票
记账凭证: 仕訳伝票
附件数: 添付数
总账: 総勘定元帳
明细账: 補助元帳
科目余额表: 科目残高一覧
资产负债表: 貸借対照表
利润表: 損益計算書
现金流量表: キャッシュフロー計算書
试算平衡: 試算表
会计期间: 会計期間
年度: 年度
月份: 月
期初: 期首
期末: 期末
本年累计: 本年累計
本月合计: 当月合計
过账: 転記
结账: 決算
反结账: 決算取消
凭证查询: 伝票照会
科目查询: 科目照会
辅助核算项目: 補助核算項目
客户: 顧客
供应商: 仕入先
部门: 部門
项目: プロジェクト
员工: 従業員
核算类别: 核算カテゴリ
新增: 追加
保存: 保存
取消: キャンセル
确认: 確認
删除: 削除
编辑: 編集
查看: 表示
导出: エクスポート
打印: 印刷
刷新: 更新
返回: 戻る
提交: 送信
重置: リセット
Conform: 確認
Discard: 破棄
Submit: 送信
Reset: リセット
Cancel: キャンセル
凭证: 伝票
总分类账: 総分類帳
核算项目余额: 核算項目残高
科目汇总表: 科目集計表
数量: 数量
单价: 単価
外币: 外貨
汇率: 為替レート
原币: 原通貨
本位币: 自国通貨
辅助账: 補助台帳
日记账: 仕訳帳
多栏账: 多欄帳
核算项目明细账: 核算項目明細帳
数量金额明细账: 数量金額明細帳
数量金额总账: 数量金額総帳
固定资产: 固定資産
工资: 給与
往来: 取引
自定义辅助核算: カスタム補助核算
核算项目: 核算項目
核算类别名称: 核算カテゴリ名
辅助核算编码: 補助核算コード
辅助核算名称: 補助核算名
余额方向: 残高方向
余额方向(借/贷): 残高方向(借方/貸方)
借方发生额: 借方発生額
贷方发生额: 貸方発生額
借方累计: 借方累計
贷方累计: 貸方累計
年初余额: 年初残高
年累计借方: 年間累計借方
年累计贷方: 年間累計貸方
年累计余额: 年間累計残高
本月借方发生额: 当月借方発生額
本月贷方发生额: 当月貸方発生額
本年借方发生额: 本年借方発生額
本年贷方发生额: 本年貸方発生額
年初借方余额: 年初借方残高
年初贷方余额: 年初貸方残高
期初借方余额: 期首借方残高
期初贷方余额: 期首貸方残高
期末借方余额: 期末借方残高
期末贷方余额: 期末貸方残高
损益结转: 損益振替
结转损益: 損益を振り替える
凭证字号: 伝票字番
凭证字: 伝票字
记账日期: 記帳日付
制单日期: 作成日付
记账状态: 記帳ステータス
审核状态: 承認ステータス
作废: 無効
恢复: 復元
冲销: 取消
红冲: 赤取消
反审核: 承認取消
全部: 全部
已作废: 無効済み
已冲销: 取消済み
操作: 操作
备注: 備考
日期: 日付
描述: 説明
类型: タイプ
名称: 名称
编码: コード
编号: 番号
金额(借方): 金額(借方)
金额(贷方): 金額(貸方)
金额方向: 金額方向
记账金额: 記帳金額
凭证编号: 伝票番号

174
i18n/ko/msg.txt Normal file
View File

@ -0,0 +1,174 @@
余额: 잔액
明细: 내역
科目: 계정
账户: 계좌
会计科目管理: 회계과목관리
科目编码: 과목코드
科目名称: 과목명
科目类型: 과목유형
上级科目: 상위과목
科目级别: 과목레벨
辅助核算: 보조핵산
状态: 상태
启用: 활성화
停用: 비활성화
新增科目: 과목추가
编辑科目: 과목편집
删除科目: 과목삭제
搜索: 검색
科目编码或名称: 과목코드 또는 명칭
资产: 자산
负债: 부채
所有者权益: 자본
成本: 비용
损益: 손익
借方: 차변
贷方: 대변
方向: 방향
期初余额: 기초잔액
期末余额: 기말잔액
本期借方: 당기차변
本期贷方: 당기대변
累计借方: 누적차변
累计贷方: 누적대변
凭证日期: 전표일자
凭证号: 전표번호
摘要: 적요
金额: 금액
合计: 합계
制单人: 작성자
审核人: 승인자
记账人: 기장자
未记账: 미기장
已记账: 기장완료
已审核: 승인완료
未审核: 미승인
新增凭证: 전표추가
编辑凭证: 전표편집
删除凭证: 전표삭제
审核凭证: 전표승인
记账: 기장
反记账: 기장취소
凭证类型: 전표유형
收款凭证: 수금전표
付款凭证: 지급전표
转账凭证: 대체전표
记账凭证: 분개전표
附件数: 첨부수
总账: 총계정원장
明细账: 보조원장
科目余额表: 과목잔액표
资产负债表: 대차대조표
利润表: 손익계산서
现金流量表: 현금흐름표
试算平衡: 합계잔액표
会计期间: 회계기간
年度: 연도
月份: 월
期初: 기초
期末: 기말
本年累计: 연간누적
本月合计: 당월합계
过账: 전기
结账: 결산
反结账: 결산취소
凭证查询: 전표조회
科目查询: 과목조회
辅助核算项目: 보조핵산항목
客户: 고객
供应商: 공급업체
部门: 부서
项目: 프로젝트
员工: 직원
核算类别: 핵산유형
新增: 추가
保存: 저장
取消: 취소
确认: 확인
删除: 삭제
编辑: 편집
查看: 보기
导出: 내보내기
打印: 인쇄
刷新: 새로고침
返回: 뒤로
提交: 제출
重置: 초기화
Conform: 확인
Discard: 폐기
Submit: 제출
Reset: 초기화
Cancel: 취소
凭证: 전표
总分类账: 총분류원장
核算项目余额: 핵산항목잔액
科目汇总表: 과목집계표
数量: 수량
单价: 단가
外币: 외화
汇率: 환율
原币: 원화통화
本位币: 기준통화
辅助账: 보조원장
日记账: 분개장
多栏账: 다단원장
核算项目明细账: 핵산항목명세장
数量金额明细账: 수량금액명세장
数量金额总账: 수량금액총장
固定资产: 고정자산
工资: 급여
往来: 거래
自定义辅助核算: 사용자정의보조핵산
核算项目: 핵산항목
核算类别名称: 핵산유형명
辅助核算编码: 보조핵산코드
辅助核算名称: 보조핵산명
余额方向: 잔액방향
余额方向(借/贷): 잔액방향(차/대)
借方发生额: 차변발생액
贷方发生额: 대변발생액
借方累计: 차변누적
贷方累计: 대변누적
年初余额: 연초잔액
年累计借方: 연간누적차변
年累计贷方: 연간누적대변
年累计余额: 연간누적잔액
本月借方发生额: 당월차변발생액
本月贷方发生额: 당월대변발생액
本年借方发生额: 연간차변발생액
本年贷方发生额: 연간대변발생액
年初借方余额: 연초차변잔액
年初贷方余额: 연초대변잔액
期初借方余额: 기초차변잔액
期初贷方余额: 기초대변잔액
期末借方余额: 기말차변잔액
期末贷方余额: 기말대변잔액
损益结转: 손익대체
结转损益: 손익대체하기
凭证字号: 전표자번
凭证字: 전표자
记账日期: 기장일자
制单日期: 작성일자
记账状态: 기장상태
审核状态: 승인상태
作废: 무효
恢复: 복원
冲销: 상계
红冲: 적자상계
反审核: 승인취소
全部: 전체
已作废: 무효완료
已冲销: 상계완료
操作: 조작
备注: 비고
日期: 날짜
描述: 설명
类型: 유형
名称: 명칭
编码: 코드
编号: 번호
金额(借方): 금액(차변)
金额(贷方): 금액(대변)
金额方向: 금액방향
记账金额: 기장금액
凭证编号: 전표번호

132
i18n/zh/msg.txt Normal file
View File

@ -0,0 +1,132 @@
Add Error: Add Error
Add Success: Add Success
Cancel: Cancel
Conform: Conform
Delete Error: Delete Error
Delete Success: Delete Success
Discard: Discard
Reset: Reset
Submit: Submit
Update Error: Update Error
Update Success: Update Success
failed: failed
id: id
ok: ok
system error: system error
业务操作: 业务操作
主参与方类型: 主参与方类型
主机构id: 主机构id
主键ID: 主键ID
交易: 交易
产品id: 产品id
从参与方类型: 从参与方类型
从机构id: 从机构id
从机构类型: 从机构类型
余额: 余额
余额方向: 余额方向
保存: 保存
信用额度: 信用额度
信用额度更新成功: 信用额度更新成功
信用额度管理: 信用额度管理
信用额度表: 信用额度表
信用额度设置成功: 信用额度设置成功
借方余额: 借方余额
充值: 充值
充值金额: 充值金额
全部: 全部
全部客户查询: 全部客户查询
创建人: 创建人
创建时间: 创建时间
删除失败: 删除失败
删除成功: 删除成功
原始交易: 原始交易
参数错误: 参数错误
取消: 取消
可用额度: 可用额度
商户id: 商户id
备注: 备注
失效日期: 失效日期
客户用户名: 客户用户名
客户编号: 客户编号
客户额度管理: 客户额度管理
已处理: 已处理
已用额度: 已用额度
开始日期: 开始日期
待处理: 待处理
总账表: 总账表
我的帐务: 我的帐务
我的额度: 我的额度
报告错帐: 报告错帐
授信额度: 授信额度
授信额度必须大于0: 授信额度必须大于0
授信额度设置成功: 授信额度设置成功
摘要: 摘要
新增客户授信: 新增客户授信
新增授信: 新增授信
方向: 方向
日期: 日期
时间: 时间
明细: 明细
明细顺序号: 明细顺序号
更新失败: 更新失败
更新时间: 更新时间
最大明细顺序号: 最大明细顺序号
机构: 机构
机构ID: 机构ID
机构类型: 机构类型
机构账户表: 机构账户表
状态: 状态
生效日期: 生效日期
用户名不能为空: 用户名不能为空
科目: 科目
科目id: 科目id
科目号: 科目号
科目名称: 科目名称
科目类别: 科目类别
科目表: 科目表
系统错误: 系统错误
结束日期: 结束日期
缺少日期参数: 缺少日期参数
订单编号: 订单编号
记账方id: 记账方id
记账方向: 记账方向
记账方类型: 记账方类型
记账日期: 记账日期
记账时间戳: 记账时间戳
记账配置表: 记账配置表
记账金额: 记账金额
设置失败: 设置失败
说明: 说明
调整: 调整
调整授信额度: 调整授信额度
账务日期: 账务日期
账务机构: 账务机构
账务机构id: 账务机构id
账务流水id: 账务流水id
账务流水表: 账务流水表
账务说明: 账务说明
账单: 账单
账单ID: 账单ID
账单id: 账单id
账单日期: 账单日期
账单时间戳: 账单时间戳
账单明细: 账单明细
账单查询: 账单查询
账单状态: 账单状态
账单金额: 账单金额
账户ID: 账户ID
账户ID不能为空: 账户ID不能为空
账户id: 账户id
账户余额: 账户余额
账户余额表: 账户余额表
账户日志: 账户日志
账户明细: 账户明细
账户明细表: 账户明细表
账户设置: 账户设置
账户配置表: 账户配置表
账本机构: 账本机构
贷方余额: 贷方余额
资源id: 资源id
金额: 金额
金额模板: 金额模板
错帐类型: 错帐类型

60
init/data.yaml Normal file
View File

@ -0,0 +1,60 @@
appcodes:
- id: accounting_dir
name: 记账方向
hierarchy_flg0
- id: balance_at
name: 余额方向
hierarchy_flg0
- id: partytype
name: 机构类型
hierarchy_flg0
- id: credit_status
name: 信用额度状态
hierarchy_flg0
appcodes_kv:
- id: accounting_dir0
parentid: accounting_dir
k: 0
v:
- id: accounting_dir1
parentid: accounting_dir
k: 1
v:
- id: balance_at_0
parentid: balance_at
k: 0
v:
- id: balance_at_1
parentid: balance_at
k: 1
v:
- id: partytype_0
parentid: partytype
k: owner
v: 平台
- id: partytype_1
parentid: partytype
k: reseller
v: 分销商
- id: partytype_2
name: partytype
k: customer
v: 客户
- id: partytype_3
parentid:partytype
k: provider
v: 供应商
- id: credit_status_active
parentid: credit_status
k: active
v: 生效
- id: credit_status_inactive
parentid: credit_status
k: inactive
v: 停用
- id: credit_status_expired
parentid: credit_status
k: expired
v: 已过期

View File

@ -1,17 +1,12 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/account",
"dbname": "sage",
"tblname": "acc_balance",
"title":"科目",
"title": "账户余额",
"params": {
"sortby":"name",
"sortby": ["acc_date desc"],
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
"alters": {}
},
"editexclouded": [
"id"
]
"editexclouded": ["id"]
}
}

View File

@ -1,17 +1,20 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/acc_detail",
"dbname": "sage",
"tblname": "acc_detail",
"title":"科目",
"title": "账务明细",
"params": {
"sortby":"name",
"sortby": ["acc_date desc"],
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
"alters": {
"acc_dir": {
"uitype": "code",
"data": [
{"value": "0", "text": "贷"},
{"value": "1", "text": "借"}
]
}
}
},
"editexclouded": [
"id"
]
"editexclouded": ["id", "acc_no"]
}
}

View File

@ -1,34 +1,49 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/account",
"dbname": "sage",
"tblname": "account",
"title":"科目",
"title": "账户管理",
"params": {
"sortby":"name",
"sortby": ["id"],
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
"alters": {
"subjectid": {
"uitype": "code",
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
"valueField": "subjectid",
"textField": "subjectid_text"
},
"orgid": {
"uitype": "code",
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
"valueField": "orgid",
"textField": "orgid_text"
},
"balance_at": {
"uitype": "code",
"data": [
{"value": "0", "text": "借"},
{"value": "1", "text": "贷"}
]
}
}
},
"editexclouded": [
"id"
],
"subtables":[
{
"field":"accountid",
"title":"账户余额",
"subtable":"acc_balance"
},
{
"field":"accountid",
"title":"账户明细",
"subtable":"acc_detail"
},
{
"field":"accountid",
"title":"账户日志",
"subtable":"accounting_log"
}
]
"editexclouded": ["id", "max_detailno", "balance"],
"subtables": [
{
"field": "accountid",
"title": "账户余额",
"subtable": "acc_balance"
},
{
"field": "accountid",
"title": "账户明细",
"subtable": "acc_detail"
},
{
"field": "accountid",
"title": "账户日志",
"subtable": "accounting_log"
}
]
}
}

23
json/credit_limit.json Normal file
View File

@ -0,0 +1,23 @@
{
"tblname": "credit_limit",
"title": "信用额度管理",
"params": {
"sortby": ["created_at desc"],
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
},
"editexclouded": ["id", "used_credit", "available_credit", "created_at", "updated_at"],
"editable": {
"new_data_url": "{{entire_url('add_credit_limit.dspy')}}",
"update_data_url": "{{entire_url('update_credit_limit.dspy')}}",
"delete_data_url": "{{entire_url('delete_credit_limit.dspy')}}"
},
"data_filter": {
"AND": [
{"field": "orgid", "op": "=", "var": "orgid"},
{"field": "status", "op": "=", "var": "status"}
]
}
}
}

View File

@ -1,25 +1,31 @@
{
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/subject",
"dbname": "sage",
"tblname": "subject",
"title":"科目",
"title": "科目管理",
"params": {
"sortby":"name",
"sortby": ["id"],
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
"alters": {
"balance_side": {
"uitype": "code",
"data": [
{"value": "0", "text": "借"},
{"value": "1", "text": "贷"}
]
},
"subjecttype": {
"uitype": "code",
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
}
}
},
"editexclouded": [
"id"
],
"subtables":[
{
"field":"subjectid",
"title":"账户设置",
"url":"../account_config",
"subtable":"account_config"
}
]
"editexclouded": ["id"],
"subtables": [
{
"field": "subjectid",
"title": "账户设置",
"subtable": "account_config"
}
]
}
}

View File

@ -30,7 +30,8 @@
"name": "balance",
"title": "账户余额",
"type": "float",
"length": 20
"length": 20,
"dec": 2
}
]
}

View File

@ -52,13 +52,15 @@
"name": "amount",
"title": "记账金额",
"type": "float",
"length": 18
"length": 18,
"dec": 2
},
{
"name": "balance",
"title": "账户余额",
"type": "float",
"length": 18
"length": 18,
"dec": 2
},
{
"name": "acclogid",

View File

@ -54,7 +54,8 @@
"name": "balance",
"title": "余额",
"type": "float",
"length": 20
"length": 20,
"dec": 2
}
],
"indexes": [

View File

@ -47,7 +47,8 @@
"name": "amount",
"title": "记账金额",
"type": "float",
"length": 18
"length": 18,
"dec": 2
},
{
"name": "billid",

View File

@ -55,7 +55,8 @@
"name": "amount",
"title": "金额",
"type": "float",
"length": 18
"length": 18,
"dec": 2
},
{
"name": "bill_date",

View File

@ -61,7 +61,8 @@
"name": "amount",
"title": "账单金额",
"type": "float",
"length": 18
"length": 18,
"dec": 2
}
]
}

144
models/credit_limit.json Normal file
View File

@ -0,0 +1,144 @@
{
"summary": [
{
"name": "credit_limit",
"title": "信用额度表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "accountid",
"title": "账户ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "orgid",
"title": "机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "credit_limit",
"title": "信用额度",
"type": "float",
"length": 18,
"dec": 2,
"nullable": "no",
"default": "0.00"
},
{
"name": "used_credit",
"title": "已用额度",
"type": "float",
"length": 18,
"dec": 2,
"nullable": "no",
"default": "0.00"
},
{
"name": "available_credit",
"title": "可用额度",
"type": "float",
"length": 18,
"dec": 2,
"nullable": "no",
"default": "0.00"
},
{
"name": "valid_from",
"title": "生效日期",
"type": "date",
"nullable": "yes"
},
{
"name": "valid_to",
"title": "失效日期",
"type": "date",
"nullable": "yes"
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 10,
"nullable": "no",
"default": "active"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp",
"nullable": "no"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "remark",
"title": "备注",
"type": "str",
"length": 500,
"nullable": "yes"
}
],
"indexes": [
{
"name": "idx_credit_limit_account",
"idxtype": "unique",
"idxfields": ["accountid"]
},
{
"name": "idx_credit_limit_orgid",
"idxtype": "index",
"idxfields": ["orgid"]
},
{
"name": "idx_credit_limit_status",
"idxtype": "index",
"idxfields": ["status"]
}
],
"codes": [
{
"field": "accountid",
"table": "account",
"valuefield": "id",
"textfield": "id"
},
{
"field": "orgid",
"table": "organization",
"valuefield": "id",
"textfield": "orgname"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='credit_status'"
}
]
}

View File

@ -36,13 +36,15 @@
"name": "d_balance",
"title": "借方余额",
"type": "float",
"length": 18
"length": 18,
"dec": 2
},
{
"name": "c_balance",
"title": "贷方余额",
"type": "float",
"length": 18
"length": 18,
"dec": 2
}
]
}

Binary file not shown.

169
scripts/load_path.py Normal file
View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
accounting 模块 RBAC 权限管理脚本
使用方法:
cd ~/repos/sage
./py3/bin/python ~/repos/accounting/scripts/load_path.py
"""
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.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"
# ============================================================
# 权限路径定义
# ============================================================
# any — 无需登录
PATHS_ANY = [
f"/{MOD}/usermenu.ui",
]
# logined — 所有已登录用户
PATHS_LOGINED = [
# 模块入口
f"/{MOD}",
f"/{MOD}/index.ui",
# 顶层 .ui 页面
f"/{MOD}/myaccounts.ui",
f"/{MOD}/accdetail.ui",
# 顶层 .dspy
f"/{MOD}/accdetail.dspy",
f"/{MOD}/billing.dspy",
f"/{MOD}/billing_download.dspy",
f"/{MOD}/get_user_balance.dspy",
f"/{MOD}/myaccounts.dspy",
f"/{MOD}/mybalance.dspy",
f"/{MOD}/oca.dspy",
f"/{MOD}/open_customer_accounts.dspy",
f"/{MOD}/open_owner_accounts.dspy",
f"/{MOD}/open_provider_accounts.dspy",
f"/{MOD}/open_reseller_accounts.dspy",
f"/{MOD}/open_reseller_provider_accounts.dspy",
# 统计卡片 .ui
f"/{MOD}/stat_total_balance.ui",
f"/{MOD}/stat_today_consumption.ui",
f"/{MOD}/stat_month_consumption.ui",
f"/{MOD}/stat_account_count.ui",
# acc_balance/
f"/{MOD}/acc_balance/index.ui",
f"/{MOD}/acc_balance/get_acc_balance.dspy",
f"/{MOD}/acc_balance/add_acc_balance.dspy",
f"/{MOD}/acc_balance/update_acc_balance.dspy",
f"/{MOD}/acc_balance/delete_acc_balance.dspy",
# acc_detail/
f"/{MOD}/acc_detail/index.ui",
f"/{MOD}/acc_detail/get_acc_detail.dspy",
f"/{MOD}/acc_detail/add_acc_detail.dspy",
f"/{MOD}/acc_detail/update_acc_detail.dspy",
f"/{MOD}/acc_detail/delete_acc_detail.dspy",
# account/
f"/{MOD}/account/index.ui",
f"/{MOD}/account/get_account.dspy",
f"/{MOD}/account/add_account.dspy",
f"/{MOD}/account/update_account.dspy",
f"/{MOD}/account/delete_account.dspy",
# account_config/
f"/{MOD}/account_config/index.ui",
f"/{MOD}/account_config/get_account_config.dspy",
f"/{MOD}/account_config/add_account_config.dspy",
f"/{MOD}/account_config/update_account_config.dspy",
f"/{MOD}/account_config/delete_account_config.dspy",
# accounting_config/
f"/{MOD}/accounting_config/index.ui",
f"/{MOD}/accounting_config/get_accounting_config.dspy",
f"/{MOD}/accounting_config/add_accounting_config.dspy",
f"/{MOD}/accounting_config/update_accounting_config.dspy",
f"/{MOD}/accounting_config/delete_accounting_config.dspy",
# accounting_log/
f"/{MOD}/accounting_log/index.ui",
f"/{MOD}/accounting_log/get_accounting_log.dspy",
f"/{MOD}/accounting_log/add_accounting_log.dspy",
f"/{MOD}/accounting_log/update_accounting_log.dspy",
f"/{MOD}/accounting_log/delete_accounting_log.dspy",
# subject/
f"/{MOD}/subject/index.ui",
f"/{MOD}/subject/get_subject.dspy",
f"/{MOD}/subject/add_subject.dspy",
f"/{MOD}/subject/update_subject.dspy",
f"/{MOD}/subject/delete_subject.dspy",
# credit_limit/
f"/{MOD}/credit_limit/index.ui",
f"/{MOD}/credit_limit/get_credit_limit.dspy",
f"/{MOD}/credit_limit/add_credit_limit.dspy",
f"/{MOD}/credit_limit/update_credit_limit.dspy",
f"/{MOD}/credit_limit/delete_credit_limit.dspy",
f"/{MOD}/credit_limit/hub.ui",
f"/{MOD}/credit_limit/credit_manage.ui",
f"/{MOD}/credit_limit/credit_overview.ui",
f"/{MOD}/credit_limit/api/credit_summary.dspy",
f"/{MOD}/credit_limit/api/set_credit_form.ui",
f"/{MOD}/credit_limit/api/set_customer_credit.dspy",
]
# ============================================================
# 执行注册
# ============================================================
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()

27
sql/credit_limit.sql Normal file
View File

@ -0,0 +1,27 @@
-- Credit Limit Table for accounting module
-- Run this DDL on the sage database to create the credit_limit table
CREATE TABLE IF NOT EXISTS credit_limit (
id VARCHAR(32) NOT NULL COMMENT '主键ID',
accountid VARCHAR(32) NOT NULL COMMENT '账户ID',
orgid VARCHAR(32) NOT NULL COMMENT '机构ID',
credit_limit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '信用额度',
used_credit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '已用额度',
available_credit DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '可用额度',
valid_from DATE COMMENT '生效日期',
valid_to DATE COMMENT '失效日期',
status VARCHAR(10) NOT NULL DEFAULT 'active' COMMENT '状态',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL COMMENT '更新时间',
created_by VARCHAR(32) COMMENT '创建人',
remark VARCHAR(500) COMMENT '备注',
PRIMARY KEY (id),
UNIQUE INDEX idx_credit_limit_account (accountid),
INDEX idx_credit_limit_orgid (orgid),
INDEX idx_credit_limit_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Insert credit_status codes into appcodes_kv
INSERT IGNORE INTO appcodes_kv (parentid, k, v) VALUES ('credit_status', 'active', '生效');
INSERT IGNORE INTO appcodes_kv (parentid, k, v) VALUES ('credit_status', 'inactive', '停用');
INSERT IGNORE INTO appcodes_kv (parentid, k, v) VALUES ('credit_status', 'expired', '已过期');

52
wwwroot/billing.dspy Normal file
View File

@ -0,0 +1,52 @@
debug(f'{params_kw=}')
userid = await get_user()
userorgid = await get_userorgid()
start_date = params_kw.get('start_date', '')
end_date = params_kw.get('end_date', '')
if not start_date or not end_date:
return json.dumps({'total': 0, 'rows': [], 'stats': {'total_count': 0, 'debit_sum': 0, 'credit_sum': 0}}, ensure_ascii=False, default=str)
ns = {
'orgid': userorgid,
'start_date': start_date,
'end_date': end_date,
'page': int(params_kw.get('page', 1)),
'rows': int(params_kw.get('rows', 30)),
'sort': 'acc_date desc'
}
async with get_sor_context(request._run_ns, 'accounting') as sor:
sql = """select d.acc_date, d.acc_timestamp, d.acc_dir, d.summary,
d.amount, d.balance, s.name as subject_name
from acc_detail d
join account a on d.accountid = a.id COLLATE utf8mb4_unicode_ci
join subject s on a.subjectid = s.id COLLATE utf8mb4_unicode_ci
where a.orgid = ${orgid}$
and d.acc_date >= ${start_date}$
and d.acc_date <= ${end_date}$"""
rows = await sor.sqlExe(sql, ns)
# 统计数据
stats_sql = """select
count(*) as total_count,
coalesce(sum(case when d.acc_dir = '1' then d.amount else 0 end), 0) as debit_sum,
coalesce(sum(case when d.acc_dir = '0' then d.amount else 0 end), 0) as credit_sum
from acc_detail d
join account a on d.accountid = a.id COLLATE utf8mb4_unicode_ci
where a.orgid = ${orgid}$
and d.acc_date >= ${start_date}$
and d.acc_date <= ${end_date}$"""
stats_recs = await sor.sqlExe(stats_sql, ns)
stats = {
'total_count': int(stats_recs[0].total_count) if stats_recs else 0,
'debit_sum': float(stats_recs[0].debit_sum) if stats_recs else 0,
'credit_sum': float(stats_recs[0].credit_sum) if stats_recs else 0
}
result = {
'total': len(rows) if isinstance(rows, list) else 0,
'rows': rows if isinstance(rows, list) else [],
'stats': stats
}
return json.dumps(result, ensure_ascii=False, default=str)

159
wwwroot/billing.ui Normal file
View File

@ -0,0 +1,159 @@
{
"widgettype": "VBox",
"id": "billing_page",
"options": {
"width": "100%",
"height": "100%",
"gap": "10px"
},
"subwidgets": [
{
"widgettype": "Form",
"id": "billing_form",
"options": {
"title": "账单查询",
"fields": [
{
"name": "start_date",
"label": "开始日期",
"type": "date",
"required": true
},
{
"name": "end_date",
"label": "结束日期",
"type": "date",
"required": true
}
],
"submit_text": "查询"
},
"binds": [
{
"event": "submit",
"target": "billing_tabular",
"actiontype": "script",
"script": "this.render(params); const statsResp = await fetch('{{entire_url(\"/accounting/billing.dspy\")}}?start_date=' + params.start_date + '&end_date=' + params.end_date); const statsData = await statsResp.json(); const statsText = bricks.getWidgetById('billing_stats'); if (statsText && statsData.stats) { statsText.dom_element.textContent = '总条数: ' + statsData.stats.total_count + ' | 借方合计: ¥' + parseFloat(statsData.stats.debit_sum).toFixed(2) + ' | 贷方合计: ¥' + parseFloat(statsData.stats.credit_sum).toFixed(2); } const dlBtn = bricks.getWidgetById('billing_download_btn'); if (dlBtn) { dlBtn.dom_element.style.display = 'inline-block'; dlBtn.dom_element.onclick = async function() { const resp = await fetch('{{entire_url(\"/accounting/billing_download.dspy\")}}?start_date=' + params.start_date + '&end_date=' + params.end_date); const data = await resp.json(); if (data.status === 'ok') { const byteChars = atob(data.data.content); const byteNumbers = new Array(byteChars.length); for (let i = 0; i < byteChars.length; i++) { byteNumbers[i] = byteChars.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = data.data.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } }; }"
}
]
},
{
"widgettype": "HBox",
"id": "billing_stats_box",
"options": {
"width": "100%",
"height": "40px",
"gap": "20px",
"align_items": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"id": "billing_stats",
"options": {
"text": "请输入日期范围进行查询",
"css": "font-size: 14px; color: #666;"
}
},
{
"widgettype": "Button",
"id": "billing_download_btn",
"options": {
"text": "下载Excel",
"css": "display: none; background-color: #52c41a; color: white; padding: 5px 15px; border-radius: 4px; cursor: pointer;"
}
}
]
},
{
"widgettype": "Tabular",
"id": "billing_tabular",
"options": {
"width": "100%",
"height": "100%",
"css": "filler",
"data_url": "{{entire_url('/accounting/billing.dspy')}}",
"editable": false,
"page_rows": 80,
"cache_limit": 3,
"row_options": {
"browserfields": {
"exclouded": ["row_num_"]
},
"fields": [
{
"name": "acc_date",
"title": "日期",
"type": "date",
"uitype": "date",
"datatype": "date",
"label": "日期",
"cwidth": 12
},
{
"name": "acc_timestamp",
"title": "时间",
"type": "timestamp",
"uitype": "timestamp",
"datatype": "timestamp",
"label": "时间",
"cwidth": 16
},
{
"name": "subject_name",
"title": "科目",
"type": "str",
"length": 50,
"uitype": "str",
"datatype": "str",
"label": "科目",
"cwidth": 14
},
{
"name": "acc_dir",
"title": "方向",
"type": "str",
"length": 4,
"uitype": "str",
"datatype": "str",
"label": "方向",
"cwidth": 8
},
{
"name": "summary",
"title": "摘要",
"type": "str",
"length": 100,
"uitype": "str",
"datatype": "str",
"label": "摘要",
"cwidth": 30
},
{
"name": "amount",
"title": "金额",
"type": "float",
"length": 18,
"dec": 4,
"uitype": "float",
"datatype": "float",
"label": "金额",
"cwidth": 12
},
{
"name": "balance",
"title": "余额",
"type": "float",
"length": 18,
"dec": 4,
"uitype": "float",
"datatype": "float",
"label": "余额",
"cwidth": 12
}
]
}
}
}
]
}

View File

@ -0,0 +1,83 @@
import io
import base64
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
userid = await get_user()
userorgid = await get_userorgid()
start_date = params_kw.get('start_date', '')
end_date = params_kw.get('end_date', '')
if not start_date or not end_date:
return json.dumps({'status': 'error', 'data': {'message': '缺少日期参数'}}, ensure_ascii=False)
ns = {
'orgid': userorgid,
'start_date': start_date,
'end_date': end_date
}
async with get_sor_context(request._run_ns, 'accounting') as sor:
sql = """select d.acc_date, d.acc_timestamp, s.name as subject_name, d.acc_dir,
d.summary, d.amount, d.balance
from acc_detail d
join account a on d.accountid = a.id COLLATE utf8mb4_unicode_ci
join subject s on a.subjectid = s.id COLLATE utf8mb4_unicode_ci
where a.orgid = ${orgid}$
and d.acc_date >= ${start_date}$
and d.acc_date <= ${end_date}$
order by d.acc_date desc"""
recs = await sor.sqlExe(sql, ns)
# 生成 xlsx
wb = Workbook()
ws = wb.active
ws.title = '账单明细'
# 表头
headers = ['日期', '时间', '科目', '方向', '摘要', '金额', '余额']
ws.append(headers)
header_font = Font(bold=True)
for cell in ws[1]:
cell.font = header_font
cell.alignment = Alignment(horizontal='center')
# 数据
for rec in recs:
direction = '借' if rec.acc_dir == '1' else '贷'
ws.append([
str(rec.acc_date),
str(rec.acc_timestamp),
rec.subject_name,
direction,
rec.summary,
float(rec.amount),
float(rec.balance)
])
# 调整列宽
ws.column_dimensions['A'].width = 12
ws.column_dimensions['B'].width = 20
ws.column_dimensions['C'].width = 15
ws.column_dimensions['D'].width = 8
ws.column_dimensions['E'].width = 40
ws.column_dimensions['F'].width = 15
ws.column_dimensions['G'].width = 15
# 保存到内存
output = io.BytesIO()
wb.save(output)
output.seek(0)
# Base64 编码
b64_data = base64.b64encode(output.read()).decode('utf-8')
filename = f'账单明细_{start_date}_{end_date}.xlsx'
return json.dumps({
'status': 'ok',
'data': {
'filename': filename,
'content': b64_data
}
}, ensure_ascii=False)

View File

@ -0,0 +1,43 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
# Initialize credit fields
ns['used_credit'] = 0
ns['available_credit'] = float(ns.get('credit_limit', 0))
ns['status'] = 'active'
from datetime import datetime
ns['created_at'] = datetime.now()
ns['updated_at'] = datetime.now()
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('credit_limit', ns.copy())
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"信用额度设置成功",
"timeout":3,
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"设置失败",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -0,0 +1,43 @@
orgid = await get_userorgid()
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
sql = """
SELECT
COALESCE(SUM(credit_limit), 0) as total_credit,
COALESCE(SUM(used_credit), 0) as total_used,
COALESCE(SUM(available_credit), 0) as total_available,
COUNT(*) as customer_count,
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_count,
COUNT(CASE WHEN status = 'expired' THEN 1 END) as expired_count
FROM credit_limit
WHERE orgid = ${orgid}$
"""
recs = await sor.sqlExe(sql, {'orgid': orgid})
if recs and len(recs) > 0:
r = recs[0]
total_credit = float(r.total_credit or 0)
total_used = float(r.total_used or 0)
total_available = float(r.total_available or 0)
usage_pct = round((total_used / total_credit * 100), 1) if total_credit > 0 else 0
return json.dumps({
"status": "ok",
"data": {
"total_credit": total_credit,
"total_used": total_used,
"total_available": total_available,
"usage_pct": usage_pct,
"customer_count": int(r.customer_count or 0),
"active_count": int(r.active_count or 0),
"expired_count": int(r.expired_count or 0)
}
})
return json.dumps({
"status": "ok",
"data": {
"total_credit": 0, "total_used": 0, "total_available": 0,
"usage_pct": 0, "customer_count": 0, "active_count": 0, "expired_count": 0
}
})

View File

@ -0,0 +1,63 @@
{
"widgettype": "Form",
"options": {
"width": "100%",
"padding": "16px",
"url": "{{entire_url('/accounting/credit_limit/api/set_customer_credit.dspy')}}",
"method": "POST",
"fields": [
{
"name": "id",
"label": "id",
"uitype": "hidden",
"value": "{{params_kw.get('id', '')}}"
},
{
"name": "accountid",
"label": "账户ID",
"uitype": "str",
"required": true,
"value": "{{params_kw.get('accountid', '')}}"
},
{
"name": "credit_limit",
"label": "授信额度",
"uitype": "float",
"required": true,
"value": "{{params_kw.get('credit_limit', '0')}}"
},
{
"name": "valid_from",
"label": "生效日期",
"uitype": "date",
"value": "{{params_kw.get('valid_from', '')}}"
},
{
"name": "valid_to",
"label": "失效日期",
"uitype": "date",
"value": "{{params_kw.get('valid_to', '')}}"
},
{
"name": "remark",
"label": "备注",
"uitype": "str",
"value": "{{params_kw.get('remark', '')}}"
}
],
"buttons": [
{
"label": "保存",
"actiontype": "submit",
"bgcolor": "#3B82F6",
"color": "#FFFFFF"
},
{
"label": "取消",
"actiontype": "close",
"bgcolor": "#475569",
"color": "#FFFFFF"
}
]
}
}

View File

@ -0,0 +1,105 @@
ns = params_kw.copy()
for k, v in ns.items():
if v == 'NaN' or v == 'null' or v == '':
ns[k] = None
accountid = ns.get('accountid')
if not accountid:
return {
"widgettype": "Error",
"options": {
"title": "参数错误",
"cwidth": 16,
"cheight": 9,
"timeout": 3,
"message": "账户ID不能为空"
}
}
credit_limit_amount = float(ns.get('credit_limit', 0) or 0)
if credit_limit_amount <= 0:
return {
"widgettype": "Error",
"options": {
"title": "参数错误",
"cwidth": 16,
"cheight": 9,
"timeout": 3,
"message": "授信额度必须大于0"
}
}
valid_from = ns.get('valid_from')
valid_to = ns.get('valid_to')
remark = ns.get('remark')
record_id = ns.get('id')
user_id = await get_user()
orgid = await get_userorgid()
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
if record_id:
sql = """
UPDATE credit_limit
SET credit_limit = ${credit_limit}$,
available_credit = ${credit_limit}$ - used_credit,
valid_from = ${valid_from}$,
valid_to = ${valid_to}$,
remark = ${remark}$,
updated_at = CURRENT_TIMESTAMP
WHERE id = ${id}$ AND orgid = ${orgid}$
"""
await sor.sqlExe(sql, {
'credit_limit': credit_limit_amount,
'valid_from': valid_from,
'valid_to': valid_to,
'remark': remark,
'id': record_id,
'orgid': orgid
})
debug(f'Updated credit limit {record_id} to {credit_limit_amount}')
else:
new_id = uuid()
now = datetime.now()
data = {
'id': new_id,
'accountid': accountid,
'orgid': orgid,
'credit_limit': credit_limit_amount,
'used_credit': 0,
'available_credit': credit_limit_amount,
'valid_from': valid_from,
'valid_to': valid_to,
'status': 'active',
'created_at': now,
'updated_at': now,
'created_by': user_id,
'remark': remark
}
await sor.C('credit_limit', data)
debug(f'Created credit limit for {accountid}: {credit_limit_amount}')
return {
"widgettype": "Message",
"options": {
"cwidth": 16,
"cheight": 9,
"title": "授信额度设置成功",
"timeout": 3,
"message": "ok"
}
}
return {
"widgettype": "Error",
"options": {
"title": "设置失败",
"cwidth": 16,
"cheight": 9,
"timeout": 3,
"message": "failed"
}
}

View File

@ -0,0 +1,276 @@
{% set credits = get_all_credits_web(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%",
"gap": "12px",
"padding": "4px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"justifyContent": "space-between"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "客户额度管理 (共{{credits|length}}条)",
"fontSize": "16px",
"fontWeight": "600",
"color": "#F1F5F9"
}
},
{
"widgettype": "Button",
"options": {
"label": "新增授信",
"bgcolor": "#3B82F6",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "6px 14px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "新增客户授信",
"width": "480px",
"height": "520px"
},
"options": {
"url": "{{entire_url('/accounting/credit_limit/api/set_credit_form.ui')}}"
}
}]
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"borderRadius": "10px",
"border": "1px solid #334155",
"width": "100%"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "#0F172A",
"padding": "10px 16px",
"borderRadius": "10px 10px 0 0",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "客户名称", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10}
},
{
"widgettype": "Text",
"options": {"text": "授信额度", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 8}
},
{
"widgettype": "Text",
"options": {"text": "已用/剩余", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10}
},
{
"widgettype": "Text",
"options": {"text": "使用率", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 6}
},
{
"widgettype": "Text",
"options": {"text": "状态", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 4}
},
{
"widgettype": "Text",
"options": {"text": "操作", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 6}
}
]
},
{% if credits|length == 0 %}
{
"widgettype": "VBox",
"options": {
"padding": "30px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "暂无客户授信记录",
"fontSize": "14px",
"color": "#64748B"
}
}
]
}
{% else %}
{% for c in credits %}
{
"widgettype": "HBox",
"options": {
"padding": "12px 16px",
"alignItems": "center",
"border": "0 0 1px 0",
"borderColor": "#334155"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"cwidth": 10, "gap": "2px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{c.orgname_text or c.accountid}}",
"fontSize": "13px",
"fontWeight": "500",
"color": "#F1F5F9"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{c.subject_name or ''}}",
"fontSize": "11px",
"color": "#64748B"
}
}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % c.credit_limit}}",
"fontSize": "13px",
"fontWeight": "600",
"color": "#3B82F6",
"cwidth": 8
}
},
{
"widgettype": "VBox",
"options": {"cwidth": 10, "gap": "2px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "已用 ¥{{'%.2f' % c.used_credit}}",
"fontSize": "12px",
"color": "#F59E0B"
}
},
{
"widgettype": "Text",
"options": {
"text": "剩余 ¥{{'%.2f' % c.available_credit}}",
"fontSize": "12px",
"color": "#22C55E"
}
}
]
},
{
"widgettype": "HBox",
"options": {"cwidth": 6, "alignItems": "center", "gap": "4px"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "#334155",
"borderRadius": "3px",
"height": "6px",
"width": "50px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}",
"borderRadius": "3px",
"height": "6px",
"width": "{{c.usage_pct}}%"
},
"subwidgets": []
}
]
},
{
"widgettype": "Text",
"options": {
"text": "{{c.usage_pct}}%",
"fontSize": "11px",
"color": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}"
}
}
]
},
{
"widgettype": "VBox",
"options": {"cwidth": 4},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{'生效' if c.status == 'active' else ('停用' if c.status == 'inactive' else '过期')}}",
"fontSize": "11px",
"fontWeight": "600",
"color": "{{'#22C55E' if c.status == 'active' else '#EF4444'}}"
}
}
]
},
{
"widgettype": "HBox",
"options": {"cwidth": 6, "gap": "4px"},
"subwidgets": [
{
"widgettype": "Button",
"options": {
"label": "调整",
"bgcolor": "#475569",
"color": "#FFFFFF",
"borderRadius": "4px",
"padding": "4px 8px",
"fontSize": "11px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "调整授信额度",
"width": "480px",
"height": "520px"
},
"options": {
"url": "{{entire_url('/accounting/credit_limit/api/set_credit_form.ui')}}",
"params_kw": {
"id": "{{c.id}}",
"accountid": "{{c.accountid}}",
"credit_limit": "{{c.credit_limit}}",
"valid_from": "{{c.valid_from or ''}}",
"valid_to": "{{c.valid_to or ''}}",
"remark": "{{c.remark or ''}}"
}
}
}]
}
]
}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% endif %}
]
}
]
}

View File

@ -0,0 +1,289 @@
{% set credits = get_my_credits_web(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%",
"gap": "12px",
"padding": "4px"
},
"subwidgets": [
{% if credits|length == 0 %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "40px",
"borderRadius": "10px",
"border": "1px solid #334155",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "暂无信用额度记录",
"fontSize": "16px",
"color": "#94A3B8"
}
},
{
"widgettype": "Text",
"options": {
"text": "请联系您的分销商销售人员为您设置信用额度",
"fontSize": "13px",
"color": "#64748B",
"marginTop": "8px"
}
}
]
}
{% else %}
{% for c in credits %}
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "16px",
"borderRadius": "10px",
"border": "1px solid #334155",
"gap": "12px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"justifyContent": "space-between"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"gap": "2px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{c.orgname_text or '未知客户'}}",
"fontSize": "16px",
"fontWeight": "600",
"color": "#F1F5F9"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{c.subject_name or ''}} | 账户: {{c.accountid}}",
"fontSize": "12px",
"color": "#64748B"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "{{'#16A34A22' if c.status == 'active' else '#EF444422'}}",
"padding": "4px 12px",
"borderRadius": "12px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "{{'生效' if c.status == 'active' else ('停用' if c.status == 'inactive' else '已过期')}}",
"fontSize": "12px",
"fontWeight": "600",
"color": "{{'#22C55E' if c.status == 'active' else '#EF4444'}}"
}
}
]
}
]
},
{
"widgettype": "HBox",
"options": {
"gap": "12px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"flex": "1", "gap": "4px"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"justifyContent": "space-between"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "额度使用",
"fontSize": "12px",
"color": "#94A3B8"
}
},
{
"widgettype": "Text",
"options": {
"text": "{{c.usage_pct}}%",
"fontSize": "12px",
"fontWeight": "600",
"color": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}"
}
}
]
},
{
"widgettype": "HBox",
"options": {
"bgcolor": "#334155",
"borderRadius": "4px",
"height": "8px",
"width": "100%"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}",
"borderRadius": "4px",
"height": "8px",
"width": "{{c.usage_pct}}%"
},
"subwidgets": []
}
]
}
]
}
]
},
{
"widgettype": "HBox",
"options": {
"gap": "8px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"flex": "1",
"bgcolor": "#0F172A",
"padding": "10px",
"borderRadius": "6px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % c.credit_limit}}",
"fontSize": "16px",
"fontWeight": "700",
"color": "#3B82F6"
}
},
{
"widgettype": "Text",
"options": {
"text": "授信额度",
"fontSize": "11px",
"color": "#64748B",
"marginTop": "2px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"flex": "1",
"bgcolor": "#0F172A",
"padding": "10px",
"borderRadius": "6px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % c.used_credit}}",
"fontSize": "16px",
"fontWeight": "700",
"color": "#F59E0B"
}
},
{
"widgettype": "Text",
"options": {
"text": "已用额度",
"fontSize": "11px",
"color": "#64748B",
"marginTop": "2px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"flex": "1",
"bgcolor": "#0F172A",
"padding": "10px",
"borderRadius": "6px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % c.available_credit}}",
"fontSize": "16px",
"fontWeight": "700",
"color": "#22C55E"
}
},
{
"widgettype": "Text",
"options": {
"text": "剩余额度",
"fontSize": "11px",
"color": "#64748B",
"marginTop": "2px"
}
}
]
}
]
},
{
"widgettype": "HBox",
"options": {
"justifyContent": "space-between"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "有效期: {{c.valid_from or '不限'}} ~ {{c.valid_to or '不限'}}",
"fontSize": "11px",
"color": "#64748B"
}
},
{
"widgettype": "Text",
"options": {
"text": "更新于 {{c.updated_at}}",
"fontSize": "11px",
"color": "#475569"
}
}
]
}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
{% endif %}
]
}

View File

@ -0,0 +1,33 @@
ns = {
'id':params_kw['id'],
}
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('credit_limit', ns)
debug('delete credit_limit success')
return {
"widgettype":"Message",
"options":{
"title":"删除成功",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete credit_limit failed')
return {
"widgettype":"Error",
"options":{
"title":"删除失败",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -0,0 +1,128 @@
ns = params_kw.copy()
debug(f'get_credit_limit.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'created_at desc'
sql = '''select a.*, b.accountid_text, c.orgid_text, d.status_text
from (select * from credit_limit where 1=1 [[filterstr]]) a
left join (select id as accountid, id as accountid_text from account where 1 = 1) b on a.accountid = b.accountid
left join (select id as orgid, orgname as orgid_text from organization where 1 = 1) c on a.orgid = c.orgid
left join (select k as status, v as status_text from appcodes_kv where parentid='credit_status') d on a.status = d.status'''
filterjson = params_kw.get('data_filter')
fields_str=r'''[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "accountid",
"title": "账户ID",
"type": "str",
"length": 32
},
{
"name": "orgid",
"title": "机构ID",
"type": "str",
"length": 32
},
{
"name": "credit_limit",
"title": "信用额度",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "used_credit",
"title": "已用额度",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "available_credit",
"title": "可用额度",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "valid_from",
"title": "生效日期",
"type": "date"
},
{
"name": "valid_to",
"title": "失效日期",
"type": "date"
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 10
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "remark",
"title": "备注",
"type": "str",
"length": 500
}
]'''
ori_fields = json.loads(fields_str)
if not filterjson:
fields = [ f['name'] for f in ori_fields ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

298
wwwroot/credit_limit/hub.ui Normal file
View File

@ -0,0 +1,298 @@
{% set cstats = get_credit_stats_web(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "16px",
"gap": "16px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "信用额度管理",
"fontSize": "22px",
"fontWeight": "700",
"color": "#F1F5F9"
}
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "12px",
"minWidth": "200px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "12px",
"borderRadius": "10px",
"border": "1px solid #334155",
"flex": "none"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "6px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\"><path d=\"M12 2v20M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6\"/></svg>",
"width": "20px",
"height": "20px"
}
},
{"widgettype": "Filler"}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % cstats.total_credit}}",
"fontSize": "22px",
"fontWeight": "700",
"color": "#3B82F6",
"lineHeight": "1.2"
}
},
{
"widgettype": "Text",
"options": {
"text": "授信总额度",
"fontSize": "13px",
"color": "#94A3B8",
"marginTop": "2px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "12px",
"borderRadius": "10px",
"border": "1px solid #334155",
"flex": "none"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "6px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#F59E0B\" stroke-width=\"2\"><path d=\"M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/><path d=\"M9 12l2 2 4-4\"/></svg>",
"width": "20px",
"height": "20px"
}
},
{"widgettype": "Filler"}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % cstats.total_used}}",
"fontSize": "22px",
"fontWeight": "700",
"color": "#F59E0B",
"lineHeight": "1.2"
}
},
{
"widgettype": "Text",
"options": {
"text": "已用额度",
"fontSize": "13px",
"color": "#94A3B8",
"marginTop": "2px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "12px",
"borderRadius": "10px",
"border": "1px solid #334155",
"flex": "none"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "6px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"20\" height=\"20\" 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.5\"/></svg>",
"width": "20px",
"height": "20px"
}
},
{"widgettype": "Filler"}
]
},
{
"widgettype": "Text",
"options": {
"text": "¥{{'%.2f' % cstats.total_available}}",
"fontSize": "22px",
"fontWeight": "700",
"color": "#22C55E",
"lineHeight": "1.2"
}
},
{
"widgettype": "Text",
"options": {
"text": "剩余额度",
"fontSize": "13px",
"color": "#94A3B8",
"marginTop": "2px"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "12px",
"borderRadius": "10px",
"border": "1px solid #334155",
"flex": "none"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"marginBottom": "6px"
},
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#A78BFA\" stroke-width=\"2\"><path d=\"M3 3v18h18\"/><path d=\"M18.7 8l-5.1 5.2-2.8-2.7L7 14.3\"/></svg>",
"width": "20px",
"height": "20px"
}
},
{"widgettype": "Filler"}
]
},
{
"widgettype": "Text",
"options": {
"text": "{{cstats.usage_pct}}%",
"fontSize": "22px",
"fontWeight": "700",
"color": "#A78BFA",
"lineHeight": "1.2"
}
},
{
"widgettype": "Text",
"options": {
"text": "额度使用率 ({{cstats.active_count}}/{{cstats.customer_count}}户)",
"fontSize": "13px",
"color": "#94A3B8",
"marginTop": "2px"
}
}
]
}
]
},
{
"widgettype": "HBox",
"options": {
"gap": "8px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Button",
"options": {
"label": "我的额度",
"bgcolor": "#3B82F6",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "8px 16px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.credit_content",
"options": {"url": "{{entire_url('/accounting/credit_limit/credit_overview.ui')}}"},
"mode": "replace"
}]
},
{
"widgettype": "Button",
"options": {
"label": "客户额度管理",
"bgcolor": "#475569",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "8px 16px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.credit_content",
"options": {"url": "{{entire_url('/accounting/credit_limit/credit_manage.ui')}}"},
"mode": "replace"
}]
},
{
"widgettype": "Button",
"options": {
"label": "全部客户查询",
"bgcolor": "#475569",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "8px 16px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.credit_content",
"options": {"url": "{{entire_url('/accounting/credit_limit/index.ui')}}"},
"mode": "replace"
}]
}
]
},
{
"widgettype": "VBox",
"id": "credit_content",
"options": {
"width": "100%",
"css": "filler",
"marginTop": "8px"
}
}
]
}

View File

@ -0,0 +1,194 @@
{
"widgettype": "VBox",
"options": {
"height": "100%",
"width": "100%"
},
"subwidgets": [
{
"id":"credit_limit_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"信用额度管理",
"toolbar":{
"tools":[]
},
"css":"card",
"editable":{
"new_data_url":"{{entire_url('add_credit_limit.dspy')}}",
"delete_data_url":"{{entire_url('delete_credit_limit.dspy')}}",
"update_data_url":"{{entire_url('update_credit_limit.dspy')}}"
},
"data_url":"{{entire_url('./get_credit_limit.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
},
"editexclouded":[
"id", "used_credit", "available_credit", "created_at", "updated_at"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "accountid",
"title": "账户ID",
"type": "str",
"length": 32,
"label": "账户ID",
"uitype": "code",
"valueField": "accountid",
"textField": "accountid_text",
"params": {
"dbname": "{{get_module_dbname('accounting')}}",
"table": "account",
"tblvalue": "id",
"tbltext": "id",
"valueField": "accountid",
"textField": "accountid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "orgid",
"title": "机构",
"type": "str",
"length": 32,
"label": "机构",
"uitype": "code",
"valueField": "orgid",
"textField": "orgid_text",
"params": {
"dbname": "{{get_module_dbname('accounting')}}",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "orgid",
"textField": "orgid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "credit_limit",
"title": "信用额度",
"type": "float",
"length": 18,
"dec": 2,
"cwidth": 15,
"uitype": "float",
"datatype": "float",
"label": "信用额度"
},
{
"name": "used_credit",
"title": "已用额度",
"type": "float",
"length": 18,
"dec": 2,
"cwidth": 15,
"uitype": "float",
"datatype": "float",
"label": "已用额度"
},
{
"name": "available_credit",
"title": "可用额度",
"type": "float",
"length": 18,
"dec": 2,
"cwidth": 15,
"uitype": "float",
"datatype": "float",
"label": "可用额度"
},
{
"name": "valid_from",
"title": "生效日期",
"type": "date",
"uitype": "date",
"datatype": "date",
"label": "生效日期"
},
{
"name": "valid_to",
"title": "失效日期",
"type": "date",
"uitype": "date",
"datatype": "date",
"label": "失效日期"
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 10,
"label": "状态",
"uitype": "code",
"valueField": "status",
"textField": "status_text",
"params": {
"dbname": "{{get_module_dbname('accounting')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "status",
"textField": "status_text",
"cond": "parentid='credit_status'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "created_at",
"title": "创建时间",
"type": "timestamp",
"uitype": "timestamp",
"datatype": "timestamp",
"label": "创建时间"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "timestamp",
"uitype": "timestamp",
"datatype": "timestamp",
"label": "更新时间"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32,
"uitype": "str",
"datatype": "str",
"label": "创建人"
},
{
"name": "remark",
"title": "备注",
"type": "str",
"length": 500,
"uitype": "str",
"datatype": "str",
"label": "备注"
}
]
},
"page_rows":160,
"cache_limit":5
}
}
]
}

View File

@ -0,0 +1,47 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
# Recalculate available_credit when credit_limit changes
if 'credit_limit' in ns and ns['credit_limit'] is not None:
credit_limit_val = float(ns['credit_limit'])
# Get current used_credit from DB
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
recs = await sor.R('credit_limit', {'id': ns['id']})
if len(recs) > 0:
used = float(recs[0].get('used_credit', 0))
ns['available_credit'] = credit_limit_val - used
from datetime import datetime
ns['updated_at'] = datetime.now()
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
r = await sor.U('credit_limit', ns)
debug('update credit_limit success')
return {
"widgettype":"Message",
"options":{
"title":"信用额度更新成功",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"更新失败",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

497
wwwroot/error_accounting.ui Normal file
View File

@ -0,0 +1,497 @@
{#
错帐处理 (Error Accounting) Page
PURPOSE:
This page provides a management interface for handling accounting errors
and exceptions. It allows operators to review, correct, and resolve
accounting discrepancies.
INTENDED WORKFLOW:
1. An accounting exception is detected (manually or automatically):
- wrong_account: Transaction posted to incorrect account/subject
- duplicate_entry: Same transaction recorded more than once
- missing_entry: Expected transaction not found in records
- amount_mismatch: Debit/credit amounts don't balance or differ from source
2. Operator reviews the error log table (error_accounting_log):
- Each row shows: timestamp, error type, original transaction info, status
- Filter by status (pending/resolved) to prioritize work
3. Operator selects an error record and chooses a correction action:
- reverse_entry: Create a reversing journal entry to cancel the original
- adjust_entry: Create an adjustment entry to correct the amount/account
- mark_resolved: Flag as resolved without further action (e.g., duplicate already fixed)
4. The correction is recorded with an audit trail linking back to the original error.
DATA SOURCE:
- Table: error_accounting_log
- Expected fields: id, timestamp, error_type, original_trans_id,
original_subject, original_amount, original_summary,
error_description, status, resolved_at, resolved_by, correction_action
TOOLBAR ACTIONS:
- 报告错帐: Opens a form to manually report a new accounting error
- 全部: Show all error records (no filter)
- 待处理: Filter to show only pending/unresolved errors
- 已处理: Filter to show only resolved errors
ROW ACTIONS (on click):
- 冲正 (reverse_entry): Reverse the original transaction
- 调整 (adjust_entry): Create an adjustment/correction entry
- 标记已处理 (mark_resolved): Mark as resolved
#}
{
"widgettype": "VBox",
"id": "error_accounting_page",
"options": {
"width": "100%",
"height": "100%",
"gap": "12px",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "VBox",
"id": "error_acc_header",
"options": {
"bgcolor": "#1E293B",
"padding": "20px",
"borderRadius": "12px",
"border": "1px solid #334155",
"gap": "8px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"alignItems": "center",
"justifyContent": "space-between"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"gap": "4px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "错帐处理",
"fontSize": "22px",
"fontWeight": "700",
"color": "#F1F5F9"
}
},
{
"widgettype": "Text",
"options": {
"text": "会计差错管理 — 发现、纠正并解决帐务异常记录",
"fontSize": "13px",
"color": "#94A3B8"
}
}
]
}
]
}
]
},
{
"widgettype": "HBox",
"id": "error_acc_toolbar",
"options": {
"bgcolor": "#1E293B",
"padding": "12px 16px",
"borderRadius": "10px",
"border": "1px solid #334155",
"alignItems": "center",
"gap": "10px"
},
"subwidgets": [
{
"widgettype": "Button",
"id": "btn_report_error",
"options": {
"label": "报告错帐",
"bgcolor": "#EF4444",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "8px 16px",
"fontWeight": "600"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "报告错帐",
"width": "560px",
"height": "600px"
},
"options": {
"url": "{{entire_url('/accounting/error_accounting_report.ui')}}"
}
}]
},
{
"widgettype": "Filler"
},
{
"widgettype": "Text",
"options": {
"text": "筛选:",
"fontSize": "13px",
"color": "#94A3B8"
}
},
{
"widgettype": "Button",
"id": "btn_filter_all",
"options": {
"label": "全部",
"bgcolor": "#3B82F6",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "6px 12px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "const tab = bricks.getWidgetById('error_acc_tabular'); if(tab) { tab.render({}); }"
}]
},
{
"widgettype": "Button",
"id": "btn_filter_pending",
"options": {
"label": "待处理",
"bgcolor": "#F59E0B",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "6px 12px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "const tab = bricks.getWidgetById('error_acc_tabular'); if(tab) { tab.render({status: 'pending'}); }"
}]
},
{
"widgettype": "Button",
"id": "btn_filter_resolved",
"options": {
"label": "已处理",
"bgcolor": "#22C55E",
"color": "#FFFFFF",
"borderRadius": "6px",
"padding": "6px 12px"
},
"binds": [{
"wid": "self",
"event": "click",
"actiontype": "script",
"script": "const tab = bricks.getWidgetById('error_acc_tabular'); if(tab) { tab.render({status: 'resolved'}); }"
}]
}
]
},
{
"widgettype": "VBox",
"id": "error_acc_table_container",
"options": {
"bgcolor": "#1E293B",
"borderRadius": "10px",
"border": "1px solid #334155",
"width": "100%",
"flex": "1"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "#0F172A",
"padding": "10px 16px",
"borderRadius": "10px 10px 0 0",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "时间", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 16}
},
{
"widgettype": "Text",
"options": {"text": "错帐类型", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 12}
},
{
"widgettype": "Text",
"options": {"text": "原始交易", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 20}
},
{
"widgettype": "Text",
"options": {"text": "金额", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10}
},
{
"widgettype": "Text",
"options": {"text": "说明", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 24}
},
{
"widgettype": "Text",
"options": {"text": "状态", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 8}
},
{
"widgettype": "Text",
"options": {"text": "操作", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10}
}
]
},
{
"widgettype": "Tabular",
"id": "error_acc_tabular",
"options": {
"width": "100%",
"height": "100%",
"css": "filler",
"data_url": "{{entire_url('/accounting/error_accounting_log.dspy')}}",
"editable": false,
"page_rows": 50,
"row_options": {
"browserfields": {
"exclouded": ["row_num_"]
},
"fields": [
{
"name": "timestamp",
"title": "时间",
"type": "timestamp",
"uitype": "timestamp",
"datatype": "timestamp",
"label": "时间",
"cwidth": 16
},
{
"name": "error_type",
"title": "错帐类型",
"type": "str",
"length": 20,
"uitype": "str",
"datatype": "str",
"label": "错帐类型",
"cwidth": 12
},
{
"name": "original_summary",
"title": "原始交易",
"type": "str",
"length": 100,
"uitype": "str",
"datatype": "str",
"label": "原始交易",
"cwidth": 20
},
{
"name": "original_amount",
"title": "金额",
"type": "float",
"length": 18,
"dec": 4,
"uitype": "float",
"datatype": "float",
"label": "金额",
"cwidth": 10
},
{
"name": "error_description",
"title": "说明",
"type": "str",
"length": 200,
"uitype": "str",
"datatype": "str",
"label": "说明",
"cwidth": 24
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 10,
"uitype": "str",
"datatype": "str",
"label": "状态",
"cwidth": 8
}
]
}
}
},
{
"widgettype": "VBox",
"id": "error_acc_empty_hint",
"options": {
"padding": "20px",
"alignItems": "center",
"bgcolor": "#1E293B"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "如表格为空,表示暂无错帐记录。请点击「报告错帐」按钮手动添加,或确认 error_accounting_log 数据源已配置。",
"fontSize": "13px",
"color": "#64748B"
}
}
]
}
]
},
{
"widgettype": "VBox",
"id": "error_acc_legend",
"options": {
"bgcolor": "#1E293B",
"padding": "16px",
"borderRadius": "10px",
"border": "1px solid #334155",
"gap": "8px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "错帐类型说明",
"fontSize": "14px",
"fontWeight": "600",
"color": "#F1F5F9"
}
},
{
"widgettype": "HBox",
"options": {
"gap": "16px",
"alignItems": "center"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {"gap": "6px", "alignItems": "center"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "#EF444433",
"padding": "2px 8px",
"borderRadius": "4px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "科目错误", "fontSize": "12px", "color": "#EF4444"}
}
]
},
{
"widgettype": "Text",
"options": {"text": "wrong_account", "fontSize": "11px", "color": "#64748B"}
}
]
},
{
"widgettype": "HBox",
"options": {"gap": "6px", "alignItems": "center"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "#F59E0B33",
"padding": "2px 8px",
"borderRadius": "4px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "重复入帐", "fontSize": "12px", "color": "#F59E0B"}
}
]
},
{
"widgettype": "Text",
"options": {"text": "duplicate_entry", "fontSize": "11px", "color": "#64748B"}
}
]
},
{
"widgettype": "HBox",
"options": {"gap": "6px", "alignItems": "center"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "#8B5CF633",
"padding": "2px 8px",
"borderRadius": "4px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "漏记", "fontSize": "12px", "color": "#8B5CF6"}
}
]
},
{
"widgettype": "Text",
"options": {"text": "missing_entry", "fontSize": "11px", "color": "#64748B"}
}
]
},
{
"widgettype": "HBox",
"options": {"gap": "6px", "alignItems": "center"},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"bgcolor": "#3B82F633",
"padding": "2px 8px",
"borderRadius": "4px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "金额不符", "fontSize": "12px", "color": "#3B82F6"}
}
]
},
{
"widgettype": "Text",
"options": {"text": "amount_mismatch", "fontSize": "11px", "color": "#64748B"}
}
]
}
]
},
{
"widgettype": "HBox",
"options": {
"gap": "16px",
"alignItems": "center",
"marginTop": "4px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "纠正操作: 冲正(reverse_entry) | 调整(adjust_entry) | 标记已处理(mark_resolved)",
"fontSize": "12px",
"color": "#94A3B8"
}
}
]
}
]
}
]
}

View File

@ -1,192 +0,0 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "0",
"bgcolor": "#0B1120"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "24px"
},
"subwidgets": [
{
"widgettype": "Title2",
"options": {
"text": "计费管理",
"color": "#F1F5F9",
"fontWeight": "700"
}
},
{
"widgettype": "Filler"
},
{
"widgettype": "Text",
"options": {
"text": "账户管理、账单明细与计费配置",
"fontSize": "14px",
"color": "#64748B"
}
}
]
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "16px",
"minWidth": "200px",
"marginBottom": "24px"
},
"subwidgets": [
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('/accounting/stat_total_balance.ui')}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('/accounting/stat_today_consumption.ui')}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('/accounting/stat_month_consumption.ui')}}"
}
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('/accounting/stat_account_count.ui')}}"
}
}
]
},
{
"widgettype": "ResponsableBox",
"options": {
"gap": "16px",
"minWidth": "250px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "24px",
"borderRadius": "12px",
"border": "1px solid #334155",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.accounting_content",
"options": {
"url": "{{entire_url('myaccounts')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#22C55E\" stroke-width=\"1.5\"><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": "36px",
"height": "36px",
"marginBottom": "16px"
}
},
{
"widgettype": "Title4",
"options": {
"text": "我的账户",
"color": "#F1F5F9",
"fontWeight": "600",
"marginBottom": "8px"
}
},
{
"widgettype": "Text",
"options": {
"text": "查看账户余额与充值记录",
"fontSize": "14px",
"color": "#94A3B8"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"padding": "24px",
"borderRadius": "12px",
"border": "1px solid #334155",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.accounting_content",
"options": {
"url": "{{entire_url('accdetail')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Svg",
"options": {
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"1.5\"><path d=\"M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z\"/></svg>",
"width": "36px",
"height": "36px",
"marginBottom": "16px"
}
},
{
"widgettype": "Title4",
"options": {
"text": "账单明细",
"color": "#F1F5F9",
"fontWeight": "600",
"marginBottom": "8px"
}
},
{
"widgettype": "Text",
"options": {
"text": "查看计费明细与消费流水",
"fontSize": "14px",
"color": "#94A3B8"
}
}
]
}
]
},
{
"widgettype": "VBox",
"id": "accounting_content",
"css": "filler",
"options": {
"width": "100%",
"overflowY": "auto"
}
}
]
}

1
wwwroot/index.ui Symbolic link
View File

@ -0,0 +1 @@
/home/hermesai/repos/accounting/wwwroot/index.ui

View File

@ -20,6 +20,7 @@
"subwidgets":[
{
"widgettype":"IconBar",
"id":"iconbar_{{loop.index}}",
"options":{
"rate": 1.5,
"tools":[
@ -32,7 +33,7 @@
{% endif %}
{
"name":"detail",
"icon":"{{entire_url('imgs/accdetail.svg')}}",
"icon":"{{entire_url('/imgs/accdetail.svg')}}",
"tip":"查看账户明细"
}
]
@ -61,7 +62,7 @@
],
"binds":[
{
"wid":"self",
"wid":"iconbar_{{loop.index}}",
"event":"recharge",
"actiontype":"urlwidget",
"target":"PopupWindow",
@ -72,37 +73,37 @@
"width":"100%",
"height":"95%"
{% else %}
"width":"360px",
"height":"240px"
"width":"600px",
"height":"500px"
{% endif %}
},
"options":{
"params_kw":{
"accountid":"{{acc.id}}"
},
"url":"entire_url('/uniapy/recharge.ui')"
"url":"{{entire_url('/unipay/recharge.ui')}}"
}
},{
"wid":"self",
"event":"recharge",
"wid":"iconbar_{{loop.index}}",
"event":"detail",
"actiontype":"urlwidget",
"target":"PopupWindow",
"popup_options":{
"icon":"{{entire_url('imgs/accdetail.svg')}}",
"icon":"{{entire_url('/imgs/accdetail.svg')}}",
"title":"明细",
{% if params_kw._is_mobile %}
"width":"100%",
"height":"95%"
{% else %}
"width":"360px",
"height":"240px"
"width":"700px",
"height":"550px"
{% endif %}
},
"options":{
"params_kw":{
"accountid":"{{acc.id}}"
},
"url":"entire_url('accdetail.ui')"
"url":"{{entire_url('/accounting/accdetail.ui')}}"
}
}
]

View File

@ -1,11 +1,22 @@
userid = await get_user()
userorgid = await get_userorgid()
if get_user_tpac:
tpac = await get_user_tpac(userid)
if tpac:
tpac_balance = await get_tpac_balance(tpac, userid)
return {
'status': 'ok',
'data': [
{
'account': 'tpac account',
'balance': tpac_balance
}
]
}
async with get_sor_context(request._run_ns, 'accounting') as sor:
sql = """select b.id, a.name, b.balance_at, c.balance from
subject a, account b,
(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
where c.accountid = b.id
and b.subjectid = a.id
sql = """select b.id, a.name, b.balance_at, b.balance from
subject a, account b
where b.subjectid = a.id
and b.orgid = ${orgid}$
"""
ns = {'orgid': userorgid}

View File

@ -1,6 +1,8 @@
debug(f'{params_kw=}')
dbname = get_module_dbname('accounting')
orgid = await get_userorgid()
orgid = params_kw.orgid
if orgid is None:
orgid = await get_userorgid()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await openCustomerAccounts(sor, '0', orgid)

View File

@ -0,0 +1,8 @@
debug(f'{params_kw=}')
dbname = get_module_dbname('accounting')
orgid = await get_userorgid()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await openCustomerAccounts(sor, '0', orgid)
return f'{orgid} customer accounts opened'
return f'{db.e_except=}'

82
wwwroot/proxy_recharge.ui Normal file
View File

@ -0,0 +1,82 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "16px",
"gap": "16px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"bgcolor": "#1E293B",
"borderRadius": "10px",
"border": "1px solid #334155",
"padding": "20px",
"cwidth": 40
},
"subwidgets": [
{
"widgettype": "Title3",
"options": {"text": "代客充值", "color": "#F1F5F9"}
},
{
"widgettype": "Text",
"options": {
"text": "输入客户用户名和充值金额,由管理员代为客户完成充值操作。",
"color": "#94A3B8",
"fontSize": "13px"
}
},
{
"widgettype": "Form",
"id": "proxy_recharge_form",
"options": {
"name": "proxy_recharge",
"submit_url": "{{entire_url('/accounting/proxy_recharge_submit.dspy')}}",
"show_label": true,
"submit_label": "确认充值",
"submit_css": "primary",
"fields": [
{
"name": "username",
"label": "客户用户名",
"uitype": "str",
"required": true,
"placeholder": "输入客户用户名",
"cwidth": 20
},
{
"name": "amount",
"label": "充值金额",
"uitype": "float",
"required": true,
"placeholder": "输入充值金额",
"cwidth": 20
}
]
},
"binds": [{
"wid": "self",
"event": "submit",
"actiontype": "urldata",
"target": "recharge_result",
"options": {
"url": "{{entire_url('/accounting/proxy_recharge_submit.dspy')}}"
}
}]
},
{
"widgettype": "VBox",
"id": "recharge_result",
"options": {
"width": "100%",
"padding": "8px"
},
"subwidgets": []
}
]
}
]
}

View File

@ -0,0 +1,148 @@
action = params_kw.get('action', 'submit')
# ---- Lookup mode: find customer by username, return info ----
if action == 'lookup':
username = params_kw.get('username', '').strip()
if not username:
return json.dumps({'status': 'error', 'message': '用户名不能为空'}, ensure_ascii=False, default=str)
db = DBPools()
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
sql = """
select
u.username,
u.orgid as customerid,
o.orgname,
a.id as accountid,
a.balance
from users u
left join organization o on u.orgid = o.id COLLATE utf8mb4_unicode_ci
left join account a on a.orgid = u.orgid COLLATE utf8mb4_unicode_ci
where u.username = ${username}$
limit 1
"""
recs = await sor.sqlExe(sql, {'username': username})
if not recs or len(recs) == 0:
return json.dumps({'status': 'error', 'message': f'用户 {username} 不存在'}, ensure_ascii=False, default=str)
rec = recs[0]
return json.dumps({
'status': 'ok',
'data': {
'username': rec.username,
'customerid': rec.customerid,
'orgname': rec.orgname or '',
'accountid': rec.accountid or '',
'balance': float(rec.balance) if rec.balance else 0.0
}
}, ensure_ascii=False, default=str)
# ---- Submit mode: process the proxy recharge ----
username = params_kw.get('username', '').strip()
amount_raw = params_kw.get('amount', 0)
if not username:
return {
"widgettype": "Text",
"options": {"text": "❌ 用户名不能为空", "color": "#EF4444"}
}
try:
amount = float(amount_raw)
except (ValueError, TypeError):
return {
"widgettype": "Text",
"options": {"text": "❌ 充值金额格式错误", "color": "#EF4444"}
}
if amount <= 0:
return {
"widgettype": "Text",
"options": {"text": "❌ 充值金额必须大于0", "color": "#EF4444"}
}
userid = await get_user()
userorgid = await get_userorgid()
db = DBPools()
# Look up the target customer by username
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
sql = """
select
u.username,
u.orgid as customerid,
o.orgname,
a.id as accountid
from users u
left join organization o on u.orgid = o.id COLLATE utf8mb4_unicode_ci
left join account a on a.orgid = u.orgid COLLATE utf8mb4_unicode_ci
where u.username = ${username}$
limit 1
"""
recs = await sor.sqlExe(sql, {'username': username})
if not recs or len(recs) == 0:
return {
"widgettype": "Text",
"options": {"text": f"❌ 找不到用户名: {username}", "color": "#EF4444"}
}
customer = recs[0]
customerid = customer.customerid
if customerid == userorgid:
return {
"widgettype": "Text",
"options": {"text": "❌ 不能给自己进行代客充值", "color": "#EF4444"}
}
# Create payment log in unipay for audit trail
unipay_dbname = get_module_dbname('unipay')
async with db.sqlorContext(unipay_dbname) as unipay_sor:
plog_id = getID()
biz_date = await get_business_date(sor)
now_str = timestampstr()
plog_data = {
"id": plog_id,
"customerid": customerid,
"channelid": "proxy",
"payment_name": "充值",
"payer_client_ip": "admin_proxy",
"amount_total": amount,
"pay_feerate": 0.0,
"pay_fee": 0.0,
"currency": "CNY",
"payment_status": "1",
"init_timestamp": now_str,
"payed_timestamp": now_str,
"cancel_timestamp": "2000-01-01 00:00:00.001",
"userid": userid
}
await unipay_sor.C('payment_log', plog_data.copy())
# Perform recharge accounting
await recharge_accounting(
sor,
customerid,
'RECHARGE',
plog_id,
biz_date,
amount,
0.0
)
debug(f'Proxy recharge: user={username}, customerid={customerid}, amount={amount}, operator={userid}')
orgname = customer.orgname or ''
return {
"widgettype": "Text",
"options": {
"text": f"✅ 代客充值成功 — 已为用户 {username} ({orgname}) 充值 ¥{amount:.2f}",
"color": "#22C55E",
"fontSize": "14px",
"fontWeight": "500"
}
}