accounting/accounting/creditlimit.py
yumoqing 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

197 lines
5.6 KiB
Python

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
LEFT JOIN account acc ON cl.accountid = acc.id
LEFT JOIN subject sub ON acc.subjectid = sub.id
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
LEFT JOIN account acc ON cl.accountid = acc.id
LEFT JOIN subject sub ON acc.subjectid = sub.id
{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