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.
This commit is contained in:
yumoqing 2026-05-28 23:05:23 +08:00
parent 5da6ddd7d5
commit 5fa058add9

View File

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