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

View File

@ -1,34 +1,49 @@
{ {
"models_dir": "${HOME}$/py/rbac/models",
"output_dir": "${HOME}$/py/sage/wwwroot/account",
"dbname": "sage",
"tblname": "account", "tblname": "account",
"title":"科目", "title": "账户管理",
"params": { "params": {
"sortby":"name", "sortby": ["id"],
"browserfields": { "browserfields": {
"exclouded": ["id"], "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": [ "editexclouded": ["id", "max_detailno", "balance"],
"id" "subtables": [
], {
"subtables":[ "field": "accountid",
{ "title": "账户余额",
"field":"accountid", "subtable": "acc_balance"
"title":"账户余额", },
"subtable":"acc_balance" {
}, "field": "accountid",
{ "title": "账户明细",
"field":"accountid", "subtable": "acc_detail"
"title":"账户明细", },
"subtable":"acc_detail" {
}, "field": "accountid",
{ "title": "账户日志",
"field":"accountid", "subtable": "accounting_log"
"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", "tblname": "subject",
"title":"科目", "title": "科目管理",
"params": { "params": {
"sortby":"name", "sortby": ["id"],
"browserfields": { "browserfields": {
"exclouded": ["id"], "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": [ "editexclouded": ["id"],
"id" "subtables": [
], {
"subtables":[ "field": "subjectid",
{ "title": "账户设置",
"field":"subjectid", "subtable": "account_config"
"title":"账户设置", }
"url":"../account_config", ]
"subtable":"account_config"
}
]
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,8 @@
"name": "amount", "name": "amount",
"title": "账单金额", "title": "账单金额",
"type": "float", "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", "name": "d_balance",
"title": "借方余额", "title": "借方余额",
"type": "float", "type": "float",
"length": 18 "length": 18,
"dec": 2
}, },
{ {
"name": "c_balance", "name": "c_balance",
"title": "贷方余额", "title": "贷方余额",
"type": "float", "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":[ "subwidgets":[
{ {
"widgettype":"IconBar", "widgettype":"IconBar",
"id":"iconbar_{{loop.index}}",
"options":{ "options":{
"rate": 1.5, "rate": 1.5,
"tools":[ "tools":[
@ -32,7 +33,7 @@
{% endif %} {% endif %}
{ {
"name":"detail", "name":"detail",
"icon":"{{entire_url('imgs/accdetail.svg')}}", "icon":"{{entire_url('/imgs/accdetail.svg')}}",
"tip":"查看账户明细" "tip":"查看账户明细"
} }
] ]
@ -61,7 +62,7 @@
], ],
"binds":[ "binds":[
{ {
"wid":"self", "wid":"iconbar_{{loop.index}}",
"event":"recharge", "event":"recharge",
"actiontype":"urlwidget", "actiontype":"urlwidget",
"target":"PopupWindow", "target":"PopupWindow",
@ -72,37 +73,37 @@
"width":"100%", "width":"100%",
"height":"95%" "height":"95%"
{% else %} {% else %}
"width":"360px", "width":"600px",
"height":"240px" "height":"500px"
{% endif %} {% endif %}
}, },
"options":{ "options":{
"params_kw":{ "params_kw":{
"accountid":"{{acc.id}}" "accountid":"{{acc.id}}"
}, },
"url":"entire_url('/uniapy/recharge.ui')" "url":"{{entire_url('/unipay/recharge.ui')}}"
} }
},{ },{
"wid":"self", "wid":"iconbar_{{loop.index}}",
"event":"recharge", "event":"detail",
"actiontype":"urlwidget", "actiontype":"urlwidget",
"target":"PopupWindow", "target":"PopupWindow",
"popup_options":{ "popup_options":{
"icon":"{{entire_url('imgs/accdetail.svg')}}", "icon":"{{entire_url('/imgs/accdetail.svg')}}",
"title":"明细", "title":"明细",
{% if params_kw._is_mobile %} {% if params_kw._is_mobile %}
"width":"100%", "width":"100%",
"height":"95%" "height":"95%"
{% else %} {% else %}
"width":"360px", "width":"700px",
"height":"240px" "height":"550px"
{% endif %} {% endif %}
}, },
"options":{ "options":{
"params_kw":{ "params_kw":{
"accountid":"{{acc.id}}" "accountid":"{{acc.id}}"
}, },
"url":"entire_url('accdetail.ui')" "url":"{{entire_url('/accounting/accdetail.ui')}}"
} }
} }
] ]

View File

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

View File

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