- 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
197 lines
5.6 KiB
Python
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
|