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

283 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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