From 9b4532156985fec88c81d0e2549e04052586c41e Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 19 Mar 2025 18:14:30 +0800 Subject: [PATCH] bugfix --- accounting/bizaccount.py | 256 +++++++++++++++++++++++++++++++++++++++ accounting/init.py | 2 + 2 files changed, 258 insertions(+) create mode 100644 accounting/bizaccount.py diff --git a/accounting/bizaccount.py b/accounting/bizaccount.py new file mode 100644 index 0000000..887889d --- /dev/null +++ b/accounting/bizaccount.py @@ -0,0 +1,256 @@ +import asyncio +import re +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 + +accounting_config = None + +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 BizAccounting: + """ + def __init__(self, curdate, accountset): + accountset:with accounting attribute and a sub accountsets + accountset: + action:with action can find the accounting_config + variable:contains the variable accounting_config need + participal: found the account to be accounting + async def prepare(sor): + find all the accounting_legs + merge multiple accounting for one account to one + check if account overdraw + sort accounting_legs by account id + + async def do_accounting(sor) + wrtie bill, billdetail + writ accounting_log + write acc_detail + write acc_balance + """ + def __init__(self, curdate, accountset, amount_threahold=0.0001): + self.accounting_config = None + self.curdate = curdate + self.accountset = accountset + self.amount_threahold = amount_threahold + + async def do_accounting(self, sor): + legs = self.get_accounting_legs(sor) + legs = [l for l in legs if l['amount'] >= self.amount_threahold] + self.check_accounting_balance(legs) + self.write_billdetail(sor) + self.merge_legs(legs) + self.accounting_legs = legs + for leg in legs: + self.leg_accounting(sor, leg) + + async def get_orgid_by_trans_role(self, role, accountset): + return accountset.get(role) + + async def get_accounting_legs(self, sor): + leg = self.get_accountset_legs(sor, self.accountset) + if self.accountset.subsets: + for aset in self.accountset.subsets: + legs1 = self.get_accountset_legs(sor, aset) + leg += legs1 + returnn legs + + async def get_accountset_legs(self, sor, accountset) + global accounting_config + action = accountset.action.split('_')[0] + await get_accounting_config(sor) + legs = [r.copy() for r in accounting_config + if r.action == action ] + debug(f'{legs=}') + rev = accountset.action.endswith('_REVERSE') + for l in legs: + 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'], + accountset.variable.copy() + ) + debug(f'{l["amt_pattern"]=}, {amtstr=}, {accountset.variable=}') + if isinstance(amtstr, str): + l['amount'] = eval(amtstr) + else: + l['amount'] = amtstr + + except Exception as e: + exception(f"{e=}, {l['amt_pattern']}, {accountset.variable=}") + raise e + + if l['amount'] is None: + debug(f'amount is None:{l["amt_pattern"]}, {accountset.variable=}') + raise AccountingAmountIsNone(self.caller.billid) + accounting_orgid = await self.get_orgid_by_trans_role(l.accounting_orgtype) + orgid = await self.get_orgid_by_trans_role(l.orgtype) + org1id = None if l.org1type is None else \ + await self.get_orgid_by_trans_role(l.org1type) + acc = await get_account(sor, accounting_orgid, orgid, l.subjectid, org1id=org1id) + if acc is None: + debug(f'can not get accountid {accounting_orgid=}, {orgid=},{l.subjectid=}, {org1id=}') + raise AccountIdNone(accounting_orgid, orgid, l['subjectid']) + l['_accounting_orgid'] = accounting_orgid + l['_orgid'] = orgid + l['_org1id'] = org1id + l['acc'] = acc + legs = sorted(legs, key=lambda x: x['acc']) + return legs + + def merge_two_legs(self, l1, l2): + if l1['acc_dir'] == l2['acc_dir']: + l = l1 + l['amount'] = l1['amount'] + l2['amount'] + return l + l = l1 if l1['amount'] > l2['amount'] else l2 + l['amount'] = abs(l1['amount'] - l2['amount']) + return l + + def merge_legs(self, legs): + mlegs = [] + cnt = len(legs) + cleg = None + for i, leg in enumerate(legs): + if leg['acc'].id == cleg['acc'].id: + cleg = self.merge_two_legs(cleg, leg) + else: + mlegs.append(cleg) + cleg = leg + mleg.append(cleg) + return mleg + + def check_accounting_balance(self, legs): + debt_balance = 0.0 + credit_balance = 0.0 + for l in legs: + if l['acc_dir'] == DEBT: + debt_balance += l['amount'] + else: + credit_balance += l['amount'] + if abs(credit_balance - debt_balance) >= self.threahold: + e = Exception('accounting legs not balance') + exception(f'{legs=}, {e=}') + raise e + + def write_billdetail(self, sor, legs): + for leg in legs: + ns = { + 'id':getID(), + 'accounting_orgid' : leg['accounting_orgid'], + 'billid' : self.billid, + 'description' : self.summary, + 'participantid' : leg['orgid'], + 'participanttype' : leg['orgtype'], + 'subjectname' : leg['subjectname'], + 'accounting_dir': leg['accounting_dir'], + 'amount' : leg['amount'] + } + await sor.C('bill_detail', ns) + + async def overdraw_check(self, sor, accid, leg, tryAgain=True): + if accid is None: + raise AccountIdNone() + + sql0 = "select max(acc_date) as acc_date from acc_balance where accountid=${accid}$" + recs = await sor.sqlExe(sql0, {'accid':accid}) + acc_date = recs[0]['acc_date'] + bal = {} + if acc_date is not None: + if acc_date > self.curdate: + raise FutureAccountingExist(accid, self.curdate, acc_date) + ns={'accid':accid, 'acc_date':acc_date} + r = await sor.sqlExe("""select * from acc_balance +where accountid=${accid}$ + and acc_date = ${acc_date}$""", ns.copy()) + if len(r) > 0: + bal = r[0] + + accs = await sor.R('account', {'id':accid}) + if len(accs) == 0: + raise AccountNoFound(accid) + + acc = accs[0] + acc['acc_date'] = self.curdate + acc['balance'] = bal.get('balance', 0) + + if acc.get('balance') is None: + acc['balance'] = 0 + if acc['balance_at'] == DEBT and leg['acc_dir'] == CREDIT \ + or acc['balance_at'] == CREDIT and leg['acc_dir'] == DEBT: + if int(acc['balance']*10000) - int(leg['amount']*10000) < 0: + if tryAgain: + await asyncio.sleep(1.5); + return await self.overdraw_check(sor, accid, leg, tryAgain=False) + else: + print(f"{acc['balance_at']=}, {leg=}") + raise AccountOverDraw(accid, acc['balance'], leg['amount']) + leg['new_balance'] = acc['balance'] - leg['amount'] + else: + leg['new_balance'] = acc['balance'] + leg['amount'] + + async def leg_accounting(self, sor, leg): + accid = leg['acc'].id + await self.overdraw_check(sor, accid, leg) + # write acc_balance + sql = """select * from acc_balance +where accountid=${accid}$ + and acc_date = ${curdate}$""" + recs = await sor.sqlExe(sql, {'accid':accid, 'curdate':self.curdate}) + if len(recs) == 0: + ns = { + 'id':getID(), + 'accountid':accid, + 'acc_date':self.curdate, + 'balance':leg['new_balance'] + } + await sor.C('acc_balance', ns.copy()) + else: + ns = recs[0] + ns['balance'] = leg['new_balance'] + await sor.U('acc_balance', ns.copy()) + + # summary = self.summary + logid = getID() + ns = { + 'id':logid, + 'accountid':accid, + 'acc_date':self.curdte, + '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_date':self.curdate, + 'acc_timestamp':self.timestamp, + 'acc_dir':leg['acc_dir'], + 'summary':self.summary, + 'amount':leg['amount'], + 'balance':leg['new_balance'], + 'acclogid':logid + } + await sor.C('acc_detail', ns.copy()) diff --git a/accounting/init.py b/accounting/init.py index c32923c..c2fc277 100644 --- a/accounting/init.py +++ b/accounting/init.py @@ -6,6 +6,7 @@ from accounting.accounting_config import Accounting from accounting.bill import write_bill from accounting.openaccount import openOwnerAccounts, openProviderAccounts, openResellerAccounts, openCustomerAccounts from accounting.getaccount import getAccountBalance, getCustomerBalance, getAccountByName, get_account_total_amount +from accounting.bizaccount import BizAccounting def load_accounting(): g = ServerEnv() @@ -19,3 +20,4 @@ def load_accounting(): g.getCustomerBalance = getCustomerBalance g.getAccountByName = getAccountByName g.get_account_total_amount = get_account_total_amount + g.BizAccounting = BizAccounting