commit 6faf88b224008eeb6c60e18455b78a0d5f72dc30 Author: yumoqing Date: Wed Jul 16 14:32:14 2025 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf8c8de --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# accounting + diff --git a/accounting/__init__.py b/accounting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounting/accounting_config.py b/accounting/accounting_config.py new file mode 100644 index 0000000..a98ac82 --- /dev/null +++ b/accounting/accounting_config.py @@ -0,0 +1,236 @@ +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 Accounting: + """ + 需要caller功能: + caller中要有分录中的变量 + get_accounting_orgid(leg) 获得记账机构 + get_account(leg,accounting_orgid) 获得记账账号(通过科目,机构类型,账务机构确定一个唯一的账号) + """ + def __init__(self, caller): + self.caller = caller + self.curdate = caller.curdate + self.realtimesettled = False + self.curdte = caller.curdate + 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 + + async def setup_accounting_legs(self): + global accounting_config + action = self.action.split('_')[0] + await get_accounting_config(self.sor) + self.accounting_legs = [r.copy() for r in accounting_config + if r.action == action ] + debug(f'{self.accounting_legs=}') + rev = self.action.endswith('_REVERSE') + for l in self.accounting_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'], + 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.bill=}') + raise AccountingAmountIsNone(self.caller.billid) + + 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) >= 0.01: + e = Exception('accounting legs not balance') + exception(f'{legs=}, {e=}') + raise e + + async def do_accounting(self, sor): + self.sor = sor + + await self.setup_accounting_legs() + debug(f'do_accounting() ...{self.accounting_legs=}') + self.check_accounting_balance(self.accounting_legs) + for leg in self.accounting_legs: + accounting_orgid = await self.caller.get_orgid_by_trans_role(sor, leg.accounting_orgtype) + orgid = await self.caller.get_orgid_by_trans_role(sor, leg.orgtype) + org1id = None if leg.org1type is None else \ + await self.caller.get_orgid_by_trans_role(sor, leg.org1type) + acc = await get_account(sor, accounting_orgid, orgid, leg.subjectid, org1id=org1id) + if acc is None: + debug(f'can not get accountid {accounting_orgid=}, {orgid=},{leg.subjectid=}, {org1id=}') + raise AccountIdNone(accounting_orgid, orgid, leg['subjectid']) + leg['_accounting_orgid'] = accounting_orgid + leg['_orgid'] = orgid + leg['_org1id'] = org1id + leg['acc'] = acc + await self.leg_accounting(sor, acc.id, 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 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, accid, leg): + # print(f'leg_accounting(), {accid=}, {leg=}') + if leg['amount'] < 0.0001: + return + 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 + 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) + 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/accountingnode.py b/accounting/accountingnode.py new file mode 100644 index 0000000..d36abb4 --- /dev/null +++ b/accounting/accountingnode.py @@ -0,0 +1,87 @@ +from .const import * +from appPublic.log import debug +from sqlor.dbpools import DBPools + +async def get_parent_orgid(sor, orgid): + sql = """select a.id from organization a, organization b +where b.parentid = a.id + and b.id = ${orgid}$""" + recs = await sor.sqlExe(sql, {'orgid':orgid}) + if len(recs) == 0: + return None + return recs[0]['id'] + +async def get_offer_orgid(sor, bid_orgid, providerid, productid, curdate): + sql = """select a.offer_orgid from saleprotocol a, product_salemode b +where a.id = b.protocolid + and a.bid_orgid = ${bid_orgid}$ + and b.providerid = ${providerid}$ + and b.productid in (${productid}$, '*') + and a.start_date <= ${curdate}$ + and a.end_date > ${curdate}$ +""" + recs = await sor.sqlExe(sql, { + 'bid_orgid':bid_orgid, + 'providerid':providerid, + 'productid':productid, + 'curdate':curdate + }) + if len(recs) == 0: + return None + rec = recs[0] + return rec['offer_orgid'] + +async def get_offer_orgs(sor, bid_orgid, providerid, productid, curdate): + offer_orgid = await get_offer_orgid(sor, bid_orgid, providerid, + productid, curdate) + if offer_orgid is None or offer_orgid == providerid: + return [] + myids = [offer_orgid] + orgs = await get_offer_orgs(sor, offer_orgid, + providerid, + productid, + curdate) + return orgs + myids + +async def get_ancestor_orgs(sor, orgid): + id = await get_parent_orgid(sor, orgid) + if not id: + return [] + ret = await get_ancestor_orgs(sor, id) + return ret + [id] + +async def get_orgtypes(sor, orgid): + sql = "select orgtypeid from orgtypes where orgid = ${orgid}$" + recs = await sor.sqlExe(sql, {'orgid':orgid}) + return [r.orgtypeid for r in recs] + +async def get_accounting_nodes(sor, customerid): + """ + gt all accounting organization for transactes customer orgid + """ + mytypes = await get_orgtypes(sor, customerid) + is_c = False + for t in mytypes: + if t in ['customer', 'agencycustomer', 'personalcustomer']: + is_c = True + break + + if not is_c: + debug(f'{customerid=} is not a customer organzition') + return [] + + sql = """select a.id from organization a, organization b +where b.parentid = a.id + and b.id = ${customerid}$ + """ + recs = await sor.sqlExe(sql, {'customerid':customerid}) + if len(recs) == 0: + debug(f'{customerid=} not found is organziation table') + return ['0'] + return [] + ret = await get_ancestor_orgs(sor, recs[0]['id']) + ret.append(recs[0]['id']) + return ret + + + diff --git a/accounting/alipay_recharge.py b/accounting/alipay_recharge.py new file mode 100644 index 0000000..76d277a --- /dev/null +++ b/accounting/alipay_recharge.py @@ -0,0 +1,55 @@ + +from datetime import datetime +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools +from appPublic.timeUtils import curDateString +from appPublic.argsConvert import ArgsConvert +from .accounting_config import get_accounting_config, AccountingOrgs +from .const import * +from .accountingnode import get_accounting_nodes +from .excep import * +from .getaccount import getAccountByName +from .businessdate import get_business_date +from .recharge import RechargeAccounting + +class AlipayAccountingOrgs(AccountingOrgs): + def __init__(self, caller, + accounting_orgid, + customerid, + resellerid=None): + + super(AlipayAccountingOrgs, self). __init__(caller, + accounting_orgid, + customerid, + resellerid=resellerid) + self.variable['手续费'] = self.caller.fee_amt + + async def get_act_specstr(self): + return ACTNAME_RECHARGE_ALIPAY + +class AlipayRechargeAccounting(RechargeAccounting): + def __init__(self, recharge_log): + super(AlipayRechargeAccounting, self).__init__(recharge_log) + self.fee_amt = recharge_log['fee_amt'] + + async def accounting(self, sor): + self.sor = sor + bz_date = await get_business_date(sor=sor) + if bz_date != self.curdate: + raise AccountingDateNotInBusinessDate(self.curdate, bz_date) + + nodes = await get_accounting_nodes(sor, self.customerid) + lst = len(nodes) - 1 + self.accountingOrgs = [] + for i, n in enumerate(nodes): + if i < lst: + ao = AlipayAccountingOrgs(self, nodes[i], self.customerid, + resellerid=nodes[i+1]) + else: + ao = AlipayAccountingOrgs(self, nodes[i], self.customerid) + self.accountingOrgs.append(ao) + await self.write_bill(sor) + [await ao.do_accounting(sor) for ao in self.accountingOrgs ] + print(f'recharge ok for {self.bill}, {nodes=}') + return True + diff --git a/accounting/argsconvert.py b/accounting/argsconvert.py new file mode 100644 index 0000000..1f9dd05 --- /dev/null +++ b/accounting/argsconvert.py @@ -0,0 +1,95 @@ +# -*- coding:utf8 -*- +import re +class ConvertException(Exception): + pass + +class ArgsConvert(object): + def __init__(self,preString,subfixString,coding='utf-8'): + self.preString = preString + self.subfixString = subfixString + self.coding=coding + sl1 = [ u'\\' + c for c in self.preString ] + sl2 = [ u'\\' + c for c in self.subfixString ] + ps = u''.join(sl1) + ss = u''.join(sl2) + re1 = ps + r"[_a-zA-Z_\u4e00-\u9fa5][a-zA-Z_0-9\u4e00-\u9fa5\,\.\'\{\}\[\]\(\)\-\+\*\/]*" + ss + self.re1 = re1 + # print( self.re1,len(self.re1),len(re1),type(self.re1)) + + async def convert(self,obj,namespace,default=''): + """ obj can be a string,[],or dictionary """ + if type(obj) == type(b''): + return await self.convertBytes(obj,namespace,default) + if type(obj) == type(''): + return await self.convertString(obj,namespace,default) + if type(obj) == type([]): + ret = [] + for o in obj: + ret.append(await self.convert(o,namespace,default)) + return ret + if type(obj) == type({}): + ret = {} + for k in obj.keys(): + ret.update({k:await self.convert(obj.get(k),namespace,default)}) + return ret + # print( type(obj),"not converted") + return obj + + def findAllVariables(self,src): + r = [] + for ph in re.findall(self.re1,src): + dl = self.getVarName(ph) + r.append(dl) + return r + + def getVarName(self,vs): + return vs[len(self.preString):-len(self.subfixString)] + + async def getVarValue(self,var,namespace,default): + v = default + try: + v = eval(var,namespace) + except Exception as e: + v = namespace.get(var, None) + if v: + return v + if callable(default): + return await default(var) + return default + return v + + async def convertString(self,s,namespace,default): + args = re.findall(self.re1,s) + for arg in args: + dl = s.split(arg) + var = self.getVarName(arg) + v = await self.getVarValue(var,namespace,default) + if type(v) != type(u''): + v = str(v) + s = v.join(dl) + return s + +if __name__ == '__main__': + from appPublic.asynciorun import run + async def main(): + ns = { + 'a':12, + 'b':'of', + 'c':'abc', + u'是':'is', + 'd':{ + 'a':'doc', + 'b':'gg', + } + } + AC = ArgsConvert('%{','}%') + s1 = "%{a}% is a number,%{d['b']}% is %{是}% undefined,%{c}% is %{d['a']+'(rr)'}% string" + arglist=['this is a descrciption %{b}% selling book',123,'ereg%{a}%,%{c}%'] + argdict={ + 'my':arglist, + 'b':s1 + } + print(s1,'<=>',await AC.convert(s1,ns)) + print(argdict,'<=>',await AC.convert(argdict,ns)) + + run(main) diff --git a/accounting/bill.py b/accounting/bill.py new file mode 100644 index 0000000..c4fb83c --- /dev/null +++ b/accounting/bill.py @@ -0,0 +1,123 @@ +from appPublic.uniqueID import getID +from appPublic.dictObject import DictObject +from sqlor.dbpools import DBPools +from ahserver.serverenv import get_serverenv +from appPublic.argsConvert import ArgsConvert +import datetime +from .const import * +from .accountingnode import get_offer_orgs, get_parent_orgid +from .excep import * +from .getaccount import getAccountByName +from .accounting_config import get_accounting_config, Accounting +# from .settle import SettleAccounting + +async def write_bill(sor, customerid, userid, orderid, business_op, amount): + bill = DictObject() + bill.customerid = customerid + bill.id = getID() + bill.userid = userid + bill.orderid = orderid + bill.business_op = business_op + bill.amount = amount + get_business_date = get_serverenv('get_business_date') + bill.bill_date = await get_business_date(sor) + bill_state = '0' + await sor.C('bill', bill.copy()) + return bill + +class BillAccounting: + def __init__(self, bill): + self.curdate = bill['bill_date'] + self.timestamp = bill['bill_timestamp'] + self.bill = bill + self.productid = bill['productid'] + self.providerid = bill['providerid'] + self.customerid = bill['customerid'] + self.billid = bill['id'] + self.action = bill['business_op'] + self.accountingOrgs = [] + self.transamount = bill['provider_amt'] + self.amount = bill['amount'] + self.discount_recs = { + } + + async def get_accounting_nodes(self): + sor = self.sor + orgid = await get_parent_orgid(sor, self.customerid) + orgids = await get_offer_orgs(sor, orgid, + self.providerid, + self.productid, + self.curdate) + if orgids is None: + return [orgid] + return orgids + [orgid] + + async def accounting(self, sor): + self.sor = sor + bz_date = await get_business_date(sor=sor) + if bz_date != self.curdate: + raise AccountingDateNotInBusinessDate(self.curdate, bz_date) + await self.prepare_accounting() + await self.do_accounting() + await sor.U('bill', {'id':self.billid, 'bill_state':'1'}) + return True + + async def do_accounting(self): + for ao in self.accountingOrgs: + await ao.do_accounting(self.sor) + + async def prepare_accounting(self): + nodes = await self.get_accounting_nodes() + print(f'accounting ndoes:{nodes}') + lst = len(nodes) - 1 + for i, n in enumerate(nodes): + if i < lst: + ao = AccountingOrgs(self, nodes[i], self.customerid, resellerid=nodes[i+1]) + else: + ao = AccountingOrgs(self, nodes[i], self.customerid) + self.accountingOrgs.append(ao) + + async def get_customer_discount(self, customerid, productid): + k = customerid + rec = self.discount_recs.get(k, None) + if rec: + return rec + sor = self.sor + sql = """select * from cp_discount +where customerid=${id}$ + and productid=${productid}$ + and start_date <= ${today}$ + and ${today}$ < end_date""" + ns = { + 'id':customerid, + 'today':self.curdate, + 'productid':productid + } + recs = await sor.sqlExe(sql, ns) + if len(recs) > 0: + self.discount_recs[k] = recs[0] + return recs[0] + return None + + async def get_reseller_discount(self, resellerid, productid): + k = resellerid + rec = self.discount_recs.get(k, None) + if rec: + return rec + sor = self.sor + sql = """select * from rp_discount +where resellerid=${id}$ + and productid=${productid}$ + and start_date <= ${today}$ + and ${today}$ < end_date""" + ns = { + 'id':resellerid, + 'today':self.curdate, + 'productid':productid + } + recs = await sor.sqlExe(sql, ns) + if len(recs) > 0: + self.discount_recs[k] = recs[0] + return recs[0] + return None + diff --git a/accounting/bizaccount.py b/accounting/bizaccount.py new file mode 100644 index 0000000..69154ea --- /dev/null +++ b/accounting/bizaccount.py @@ -0,0 +1,262 @@ +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 + 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, biz_order, accountset, amount_threahold=0.0001): + self.accounting_config = None + self.curdate = curdate + self.biz_order = biz_order + self.accountset = accountset + self.amount_threahold = amount_threahold + self.timestamp = timestampstr() + + 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) + bill = await write_bill(sor, self.biz_order.customerid, + self.biz_order.userid, + self.biz_order.id, + self.biz_order.business_op, + self.biz_order.amount) + await self.write_billdetail(sor) + self.merge_legs(legs) + self.accounting_legs = legs + for leg in legs: + await 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 + return 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.copy() + ) + debug(f'{l["amt_pattern"]=}, {amtstr=}, {accountset=}') + if isinstance(amtstr, str): + l['amount'] = eval(amtstr) + else: + l['amount'] = amtstr + + except Exception as e: + exception(f"{e=}, {l['amt_pattern']}, {accountset=}") + raise e + + if l['amount'] is None: + debug(f'amount is None:{l["amt_pattern"]}, {accountset=}') + 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 + + async 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/bzdate.py b/accounting/bzdate.py new file mode 100644 index 0000000..13e0f02 --- /dev/null +++ b/accounting/bzdate.py @@ -0,0 +1,53 @@ +from datetime import date, timedelta +""" +Patterns = + 'D' + 'W[0-6]' + 'M[00-31]' + 'S[1-3]-[00-31]' + 'Y[01-12]-[00-31]' +} +""" + +def str2date(sd): + a = [ int(i) for i in sd.split('-') ] + return date(*a) + +def is_monthend(dt): + if isinstance(dt, str): + dt = str2date(dt) + nxt_day = dt + timedelta(days=1) + if dt.month != nxt_day.month: + return True + return False + +def is_match_pattern(pattern, strdate): + if pattern == 'D': + return True + dt = ste2date(strdate) + if pattern.startswith('W'): + w = (int(pattern[1]) + 1) % 7 + + if dt.weekday() == w: + return True + return False + if pattern.startswith('M'): + day = int(pattern[1:]) + if day == 0 and is_monthend(dt): + return True + if day == dt.day: + return True + return False + if pattern.startswith('S'): + m,d = [ int(i) for i in pattern[1:].split('-') ] + m %= 4 + if m == dt.month and d == dt.day: + return True + return False + if pattern.startswith('Y'): + m,d = [ int(i) for i in pattern[1:].split('-') ] + if m == dt.month and d == dt.day: + return True + return False + + diff --git a/accounting/const.py b/accounting/const.py new file mode 100644 index 0000000..37a1ab0 --- /dev/null +++ b/accounting/const.py @@ -0,0 +1,44 @@ +from appPublic.registerfunction import RegisterFunction + +def DBNAME(): + rf = RegisterFunction() + f = rf.get('get_module_database') + if f is None: + e = Exception('function "get_module_database" not registed') + exception(f'{e}') + raise e + return f('accounting') + +RESELLER_ORG = '1' +OWNER_OGR = '0' +CORP_CUSTOMER = '2' +PERSONAL = '3' +PROVIDER = '4' + +PARTY_OWNER = '平台' +PARTY_CUSTOMER = '客户' +PARTY_RESELLER = '分销商' +PARTY_PROVIDER = '供应商' + +DEBT = '0' +CREDIT = '1' + +ACTNAME_BUY = 'consume' +ACTNAME_RECHARGE = 'recharge' +ACTNAME_RECHARGE_ALIPAY = 'rechange_alipay' +ACTNAME_SETTLE = '结算' + +SALEMODE_DISCOUNT = '折扣' +SALEMODE_REBATE = '代付费' +SALEMODE_FLOORPRICE = '底价' + +ACTION_RECHARGE_ALIPAY = 'RECHARGE_ALIPAY' +ACTION_RECHARGE_ALIPAY_REVERSE = 'RECHARGE_ALIPAY_REVERSE' +ACTION_RECHARGE = 'RECHARGE' +ACTION_RECHARGE_REVERSE = 'RECHARGE_REVERSE' +ACTION_BUY = 'BUY' +ACTION_REVERSE_BUY = 'BUY_REVERSE' +ACTION_RENEW = 'RENEW' +ACTION_RENEW_REVERSE = 'RENEW_REVERSE' +ACTION_SETTLE = 'SETTLE' +ACTION_SETTLE_REVERSE = 'SETTLE_REVERSE' diff --git a/accounting/consume.py b/accounting/consume.py new file mode 100644 index 0000000..a048228 --- /dev/null +++ b/accounting/consume.py @@ -0,0 +1,63 @@ +from datetime import datetime +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools +from appPublic.timeUtils import curDateString +from appPublic.argsConvert import ArgsConvert +from .accounting_config import get_accounting_config, AccountingOrgs +from .const import * +from .accountingnode import get_accounting_nodes +from .excep import * +from .getaccount import getAccountByName +from .businessdate import get_business_date + +class ConsumeAccounting: + def __init__(self, cnsume_log): + self.db = DBPools() + self.recharge_log = recharge_log + self.customerid = recharge_log['customerid'] + self.orderid = None + self.curdate = recharge_log['recharge_date'] + self.transamount = recharge_log['recharge_amt'] + self.timestamp = datetime.now() + self.productid = None + self.providerid = None + self.action = recharge_log['action'] + self.summary = self.action + self.billid = getID() + self.bill = { + 'id':self.billid, + 'customerid':self.recharge_log['customerid'], + 'resellerid':None, + 'orderid':None, + 'business_op':self.recharge_log['action'], + 'amount':self.recharge_log['recharge_amt'], + 'bill_date':self.curdate, + 'bill_timestamp':self.timestamp + } + + + async def accounting(self, sor): + self.sor = sor + bz_date = await get_business_date(sor=sor) + if bz_date != self.curdate: + raise AccountingDateNotInBusinessDate(self.curdate, bz_date) + + nodes = await get_accounting_nodes(sor, self.customerid) + lst = len(nodes) - 1 + self.accountingOrgs = [] + for i, n in enumerate(nodes): + if i < lst: + ao = AccountingOrgs(self, nodes[i], self.customerid, + resellerid=nodes[i+1]) + else: + ao = AccountingOrgs(self, nodes[i], self.customerid) + self.accountingOrgs.append(ao) + await self.write_bill(sor) + [await ao.do_accounting(sor) for ao in self.accountingOrgs ] + print(f'recharge ok for {self.bill}, {nodes=}') + return True + + async def write_bill(self, sor): + await sor.C('bill', self.bill.copy()) + # await sor.C('recharge_log', self.recharge_log.copy()) + diff --git a/accounting/dayend_balance.py b/accounting/dayend_balance.py new file mode 100644 index 0000000..9e785ef --- /dev/null +++ b/accounting/dayend_balance.py @@ -0,0 +1,18 @@ +from datetime import datetime +from sqlor.dbpools import DBPools +from appPublic.uniqueID import getID +from accounting.businessdate import previous_business_date +from accounting.const import * + +async def dayend_balance(): + dat = await previous_business_date() + ts = datetime.now() + sql = """select a.* from (select accountid, max(acc_date) as acc_date, balance from acc_balance where accountid is not null group by accountid) a where acc_date < ${acc_date}$""" + db = DBPools() + async with db.sqlorContext(DBNAME()) as sor: + recs = await sor.sqlExe(sql, {'acc_date':dat}) + for r in recs: + r['id'] = getID() + r['acc_date'] = dat + await sor.C('acc_balance', r) + diff --git a/accounting/excep.py b/accounting/excep.py new file mode 100644 index 0000000..b0cb95f --- /dev/null +++ b/accounting/excep.py @@ -0,0 +1,121 @@ +################### +#exceptions for accounting +#################### +class AccountIdNone(Exception): + def __init__(self, accounting_orgid, orgid, subjectname): + self.accounting_orgid = accounting_orgid + self.orgid = orgid + self.subjectname = subjectname + + def __str__(self): + return f'AccountIdNone({self.accounting_orgid=}, {self.orgid=}, {self.subjectname=}' + def __expr__(self): + return str(self) + +class AccountingAmountIsNone(Exception): + def __init__(self, billid): + self.billid = billid + + def __str__(self): + return f'AccountingAmountIsNone({self.billid=}) accounting amount is None' + + def __expr__(self): + return str(self) + +class AccountOverDraw(Exception): + def __init__(self, accid, balance, transamt): + self.accid = accid + self.balance = balance + self.transamt = transamt + + def __str__(self): + return f'AccountOverDraw({self.accid=},{self.balance=}, {self.transamt=}) overdraw' + + def __expr__(self): + return str(self) + +class AccountNoFound(Exception): + def __init__(self, accid): + self.accid = accid + + def __str__(self): + return f'Account({self.accid}) not found' + + def __expr__(self): + return str(self) + +class OrderNotFound(Exception): + def __init__(self, orderid): + self.orderid = orderid + + def __str__(self): + return f'Order({self.orderid}) not found' + + def __expr__(self): + return str(self) +class BusinessDateParamsError(Exception): + pass + +class AccountingDateNotInBusinessDate(Exception): + def __init__(self, accounting_date, business_date): + self.accounting_date = accounting_date + self.business_date = business_date + + def __str__(self): + return f'Accounting date({self.accounting_date}) not in business_date({self.business_date})' + + def __expr__(self): + return str(self) + +class FutureAccountingExist(Exception): + def __init__(self, accid, accounting_date, future_date): + self.accid = accid + self.accounting_date = accounting_date + self.future_date = future_date + + def __str__(self): + return f'Account(id={self.accid}) in acc_balance exist future({self.future_date}) accounting record, curdate={self.accounting_date}' + def __expr__(self): + return str(self) + +class GetCustomerPriceError(Exception): + def __init__(self, accounting_orgid, orgid, productid): + self.accounting_orgid = accounting_orgid + self.orgid = orgid + self.productid = productid + + def __str__(self): + return f'GetCustomerPriceError({self.accounting_orgid=}, {self.orgid=}, {self.productid=})' + + def __expr__(self): + return str(self) + +class ProductProtocolNotDefined(Exception): + def __init__(self, offer_orgid, bid_orgid, providerid, productid, curdate): + self.bid_orgid = bid_orgid + self.offer_orgid = offer_orgid + self.providerid = providerid + self.productid = productid + self.curdate = curdate + + def __str__(self): + return f'ProductProtocolNotDefined({self.offer_orgid=},{self.bid_orgid=},{self.providerid=},{self.productid=},{self.curdate=})' + + def __expr__(self): + return str(self) + +class ProductBidProtocolNotDefined(Exception): + def __init__(self, offer_orgid, bid_orgid, providerid, productid, curdate): + self.bid_orgid = bid_orgid + self.offer_orgid = offer_orgid + self.providerid = providerid + self.productid = productid + self.curdate = curdate + + def __str__(self): + return f'ProductProtocolNotDefined({self.offer_orgid=},{self.bid_orgid=},{self.providerid=},{self.productid=},{self.curdate=})' + + def __expr__(self): + return str(self) + + diff --git a/accounting/getaccount.py b/accounting/getaccount.py new file mode 100644 index 0000000..52ad42b --- /dev/null +++ b/accounting/getaccount.py @@ -0,0 +1,156 @@ +from appPublic.log import debug, exception +from sqlor.dbpools import DBPools +from ahserver.serverenv import ServerEnv, get_serverenv +from .const import * +from accounting.accountingnode import get_parent_orgid + +async def get_account(sor, accounting_orgid, orgid, subjectid, org1id=None): + ss = "org1id is NULL" + if org1id: + ss = "org1id = ${org1id}$" + + sql = """select * from account + where + subjectid = ${subjectid}$ and + accounting_orgid = ${accounting_orgid}$ and + orgid = ${orgid}$ and + """ + ss + ns = { + "accounting_orgid":accounting_orgid, + "orgid":orgid, + "org1id":org1id, + "subjectid":subjectid + } + recs = await sor.sqlExe(sql, { + "accounting_orgid":accounting_orgid, + "orgid":orgid, + "org1id":org1id, + "subjectid":subjectid + }) + if len(recs) == 0: + debug(f'{sql=}, {ns=}') + return None + return recs[0] + +async def get_account_by_subjectname(sor, accounting_orgid, orgid, subjectname, org1id=None): + ss = "a.org1id is NULL" + if org1id: + ss = "a.org1id = ${org1id}$" + + sql = """select * from account a, subject b + where + a.subjectid = b.id and + b.name = ${subjectname}$ and + a.accounting_orgid = ${accounting_orgid}$ and + a.orgid = ${orgid}$ and + """ + ss + recs = await sor.sqlExe(sql, { + "accounting_orgid":accounting_orgid, + "orgid":orgid, + "org1id":org1id, + "subjectname":subjectname + }); + if len(recs) == 0: + return None + for rec in recs: + if a.org1id == org1id: + return rec['id'] + return None + +async def getAccountByName(sor, accounting_orgid, orgid, name, org1id): + sql = """select a.* from account a, subject b +where a.subjectid = b.id and + a.accounting_orgid = ${accounting_orgid}$ and + a.orgid = ${orgid}$ and + b.name = ${name}$""" + recs = await sor.sqlExe(sql, { + "accounting_orgid":accounting_orgid, + "orgid":orgid, + "name":name + }); + if len(recs) == 0: + return None + for rec in recs: + if rec.org1id == org1id: + return rec['id'] + return None + +async def getTransPayMode(): + pass + +async def getCustomerBalance(sor, customerid): + name = '客户资金账户' + get_owner_orgid = get_serverenv('get_owner_orgid') + if get_owner_orgid is None: + debug('get_owner_orgid function is not a serverenv function') + return None + debug(f'{get_owner_orgid=}') + orgid = await get_owner_orgid(sor, customerid) + if orgid is None: + debug(f"{customerid=}'s parent organization not found") + return None + + balance = await getAccountBalance(sor, orgid, customerid, name, None) + if balance is None: + debug(f'accid is None, {orgid=}, {customerid=}, {name=}') + return None + return balance + +async def getAccountBalance(sor, accounting_orgid, orgid, subjectname, org1id): + accid = await getAccountByName(sor, accounting_orgid, + orgid, + subjectname,org1id) + if accid is None: + debug(f'accid is None, {accounting_orgid=}, {orgid=}, {subjectname=}') + return None + return await getAccountBalanceByAccid(sor, accid) + +async def get_account_total_amount(sor, accid, accounting_dir, from_date, to_date): + sql = """select sun(amount) as amount from acc_detail +where accountid =${accountid}$ + and acc_date >= ${from_date}$ + and acc_date < ${to_date}$ + and acc_dir = ${accounting_dir}$ +""" + ns = { + 'accountid':accid, + 'accounting_dir':accounting_dir, + 'from_date':from_date, + 'to_date':to_date + } + recs = await sor.sqlExe(sql, ns.copy()) + if len(recs)==0: + e = Exception(f'get_account_total_amount() error, {ns=}') + exception('{e=}') + raise e + return recs[0].amount + + +async def getAccountBalanceByAccid(sor, accid): + balances = await sor.sqlExe("""select * from acc_balance where accountid=${accid}$ order by acc_date desc""", {'accid':accid}) + if len(balances) == 0: + debug(f'acc_balance is None, {accid=}') + return 0 + return balances[0]['balance'] + +async def get_account_info(sor, accid): + sql = ''' +select b.orgname as accounting_org, +case when a.accounting_orgid = a.orgid then '本机构' + when c.org_type in ('0', '1') then '分销商' + when c.org_type = '2' then '供应商' + else '客户' end as acctype, +c.orgname, +d.name +from account a, organization b, organization c, subject d +where a.accounting_orgid = b.id + and a.orgid = c.id + and a.subjectid = d.id + and a.id = ${accid}$''' + recs = await sor.sqlExe(sql, {'accid':accid}) + if len(recs) == 0: + + return None + r = recs[0] + r['balance'] = await getAccountBalanceByAccid(sor, accid) + return r diff --git a/accounting/getdbname.py b/accounting/getdbname.py new file mode 100644 index 0000000..5d2fcf5 --- /dev/null +++ b/accounting/getdbname.py @@ -0,0 +1,7 @@ +from ahserver.serverenv import get_serverenv + +def get_dbname(): + f = get_serverenv('get_module_dbname') + if f is None: + raise Exception('get_module_dbname() not found') + return f('accounting') diff --git a/accounting/init.py b/accounting/init.py new file mode 100644 index 0000000..c2fc277 --- /dev/null +++ b/accounting/init.py @@ -0,0 +1,23 @@ +from appPublic.registerfunction import RegisterFunction +from appPublic.dictObject import DictObject +from appPublic.log import debug, exception, error +from ahserver.serverenv import ServerEnv +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() + g.Accounting = Accounting + g.write_bill = write_bill + g.openOwnerAccounts = openOwnerAccounts + g.openProviderAccounts = openProviderAccounts + g.openResellerAccounts = openResellerAccounts + g.openCustomerAccounts = openCustomerAccounts + g.getAccountBalance = getAccountBalance + g.getCustomerBalance = getCustomerBalance + g.getAccountByName = getAccountByName + g.get_account_total_amount = get_account_total_amount + g.BizAccounting = BizAccounting diff --git a/accounting/ledger.py b/accounting/ledger.py new file mode 100644 index 0000000..121401e --- /dev/null +++ b/accounting/ledger.py @@ -0,0 +1,28 @@ +from datetime import datetime +from appPublic.uniqueID import getID +from appPublic.timeUtils import strdate_add +from accounting.businessdate import get_business_date + +async def accounting_ledger(sor): + rd = await get_business_date(sor) + d = strdate_add(rd, days=-1) + print(f'{rd=}, {d=}') + ts = datetime.now() + sql = """ +select a.accounting_orgid, +a.subjectid, +sum(case a.balance_at when '1' then b.balance else 0 end) as c_balance, +sum(case a.balance_at when '0' then b.balance else 0 end) as d_balance +from account a, acc_balance b +where a.id = b.accountid + and b.acc_date = ${acc_date}$ +group by a.accounting_orgid, a.subjectid +""" + recs = await sor.sqlExe(sql, {'acc_date':d}) + await sor.sqlExe('delete from ledger where acc_date=${acc_date}$', + {'acc_date':d}) + for r in recs: + r['id'] = getID() + r['acc_date'] = d + await sor.C('ledger', r.copy()) + diff --git a/accounting/openaccount.py b/accounting/openaccount.py new file mode 100644 index 0000000..3c0efcb --- /dev/null +++ b/accounting/openaccount.py @@ -0,0 +1,101 @@ +from sqlor.dbpools import DBPools +from .const import * +from appPublic.uniqueID import getID +from appPublic.registerfunction import RegisterFunction, rfexe +from appPublic.log import debug, exception +from datetime import datetime +from accounting.getaccount import get_account + +async def openAccount(sor, accounting_orgid, orgid, account_config, org1id=None): + acc = await get_account(sor, accounting_orgid, orgid, + account_config['subjectid'], org1id=org1id) + if acc: + debug(f'{acc=} opened') + return + + ns = { + 'id':getID(), + 'accounting_orgid':accounting_orgid, + 'orgid':orgid, + 'subjectid':account_config['subjectid'], + 'balance_at':account_config['balance_side'], + 'max_detailno':0 + } + if org1id: + ns['org1id'] = org1id; + debug(f'openAccount(), {ns=}') + await sor.C('account', ns.copy()) + debug(f'{ns=} opened') + +async def openPartyAccounts(sor, accounting_orgid, orgid, party_type, org1id=None, party1_type=None): + addon_cond = " and a.party1type is NULL " + ns = {'partytype':party_type} + if party1_type: + addon_cond = " and a.party1type = ${party1type}$ " + ns['party1type'] = party1_type + + sql = """select a.*, b.id as subjectid, b.name as subjectname,b.balance_side from account_config a, subject b +where a.subjectid = b.id """ \ + + addon_cond + """ + and a.partytype=${partytype}$ """ + recs = await sor.sqlExe(sql, ns) + # print(f'select account_config {recs=}', party_type) + debug(f'{sql=}, {orgid=}, {party_type=}') + for r in recs: + debug(f'{r=}') + await openAccount(sor, accounting_orgid, orgid, r, org1id=org1id) + +async def openResellerAccounts(sor, accounting_orgid, orgid): + return await openPartyAccounts(sor, accounting_orgid, orgid, PARTY_RESELLER) + +async def openCustomerAccounts(sor, accounting_orgid, orgid): + return await openPartyAccounts(sor, accounting_orgid, orgid, PARTY_CUSTOMER) + +async def openOwnerAccounts(sor, accounting_orgid): + orgid = accounting_orgid + return await openPartyAccounts(sor, accounting_orgid, orgid, PARTY_OWNER) + +async def openProviderAccounts(sor, accounting_orgid, orgid): + return await openPartyAccounts(sor, accounting_orgid, orgid, PARTY_PROVIDER) + +async def openAllCustomerAccounts(sor, accounting_orgid): + rf = RegisterFunction() + f = rf.get('get_customers_by_orgid') + if f is None: + exception(f'get_customers_by_orgid function not registed, {accounting_orgid=}') + raise Exception('get_customers_by_orgid not registed') + recs = await f(sor, accounting_orgid) + print(f'{recs=}') + for r in recs: + await openCustomerAccounts(sor, accounting_orgid, r['id']) + +async def openAllResellerAccounts(sor, accounting_orgid): + rf = RegisterFunction() + f = rf.get('get_resellers_by_orgid') + if f is None: + exception(f'get_resellers_by_orgid function not registed, {accounting_orgid=}') + raise Exception('get_resellers_by_orgid not registed') + recs = await f(sor, accounting_orgid) + print(f'{recs=}') + for r in recs: + await openResellerAccounts(sor, accounting_orgid, r['id']) + +async def openAllProviderAccounts(sor, accounting_orgid): + rf = RegisterFunction() + f = rf.get('get_providers_by_orgid') + if f is None: + exception(f'get_providers_by_orgid function not registed, {accounting_orgid=}') + raise Exception('get_providers_by_orgid not registed') + recs = await f(sor, accounting_orgid) + print(f'{recs=}') + for r in recs: + await openProviderAccounts(sor, accounting_orgid, r['id']) + +async def openRetailRelationshipAccounts(sor, accounting_orgid, providerid, resellerid): + await openPartyAccounts(sor, accounting_orgid, providerid, PARTY_PROVIDER, + org1id=resellerid, + party1_type=PARTY_RESELLER) + await openPartyAccounts(sor, accounting_orgid, resellerid, PARTY_RESELLER, + org1id=providerid, + party1_type=PARTY_PROVIDER) + diff --git a/accounting/order_to_bill.py b/accounting/order_to_bill.py new file mode 100644 index 0000000..560dc9d --- /dev/null +++ b/accounting/order_to_bill.py @@ -0,0 +1,56 @@ +from .const import * +from datetime import datetime +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +async def _order2bill(sor, orderid): + sql = """select +og.orderid, +og.id as ordergoodsid, +o.customerid, +o.order_date, +o.business_op, +o.provider_orderid, +og.productid, +og.quantity, +og.providerid, +og.list_price, +og.discount, +og.price, +og.amount +from bz_order o, order_goods og +where o.id = og.orderid + and o.id = ${id}$ + and o.order_status = '0' +""" + recs = await sor.sqlExe(sql, {'id':orderid}) + if len(recs) == 0: + return + for r in recs: + ns = { + 'id':getID(), + 'customerid':r['customerid'], + 'ordergoodsid':r['ordergoodsid'], + 'orderid':r['orderid'], + 'business_op':r['business_op'], + 'provider_amt':r['list_price'] * r['quantity'], + 'quantity':r['quantity'], + 'amount':r['amount'], + 'bill_date':r['order_date'], + 'bill_timestamp':datetime.now(), + 'bill_state':'0', + 'productid':r['productid'], + 'providerid':r['providerid'], + 'provider_billid':None, + 'resourceid':None + } + await sor.C('bill', ns) + await sor.U('bz_order', {'id':orderid, 'order_status':'1'}) + +async def order2bill(orderid, sor=None): + if sor is None: + db = DBPools() + async with db.sqlorContext(DBNAME()) as sor: + await _order2bill(sor, orderid) + else: + await _order2bill(sor, orderid) diff --git a/accounting/recharge.py b/accounting/recharge.py new file mode 100644 index 0000000..31d2954 --- /dev/null +++ b/accounting/recharge.py @@ -0,0 +1,87 @@ + +from datetime import datetime +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools +from appPublic.timeUtils import curDateString +from appPublic.argsConvert import ArgsConvert +from .getdbname import get_dbname +from .accounting_config import get_accounting_config, Accounting +from .const import * +from .excep import * +from .getaccount import getAccountByName +from .businessdate import get_business_date + +class RechargeAccounting: + def __init__(self, recharge_log): + self.db = DBPools() + self.recharge_log = recharge_log + self.customerid = recharge_log['customerid'] + self.orderid = recharge_log['orderid'] + self.curdate = recharge_log['transdate'] + self.transamount = recharge_log['recharge_amt'] + self.timestamp = datetime.now() + self.productid = None + self.providerid = None + self.action = recharge_log['action'] + self.summary = self.action + self.billid = getID() + self.variable = { + "交易金额":100, + "充值费率":0.003, + } + self.bill = { + 'id':self.billid, + 'customerid':self.recharge_log['customerid'], + 'resellerid':None, + 'orderid':None, + 'business_op':self.recharge_log['action'], + 'amount':self.recharge_log['recharge_amt'], + 'bill_date':self.curdate, + 'bill_timestamp':self.timestamp + } + + + async def get_account(self, leg): + if leg.orgtype == '平台': + async def accounting(self, sor): + self.sor = sor + bz_date = await get_business_date(sor=sor) + if bz_date != self.curdate: + raise AccountingDateNotInBusinessDate(self.curdate, bz_date) + + ao = Accounting(self, nodes[i], self.customerid, + resellerid=nodes[i+1]) + else: + ao = AccountingOrgs(self, nodes[i], self.customerid) + self.accountingOrgs.append(ao) + await self.write_bill(sor) + [await ao.do_accounting(sor) for ao in self.accountingOrgs ] + print(f'recharge ok for {self.bill}, {nodes=}') + return True + + async def write_bill(self, sor): + await sor.C('bill', self.bill.copy()) + # await sor.C('recharge_log', self.recharge_log.copy()) + +async def recharge_accounting(sor, customerid, action, orderid, transdate, recharge_amt): + """ + summary:recharge channe(handly, wechat, alipay) + """ + recharge_log = { + "customerid":customerid, + "transdate":transdate, + "orderid":orderid, + "recharge_amt":recharge_amt, + "action":action, + } + ra = RechargeAccounting(recharge_log) + if sor: + r = await ra.accounting(sor) + return True + + db = DBPools() + dbname = get_dbname() + async with db.sqlorContext(dbname) as sor: + r = await ra.accounting(sor) + return True + diff --git a/accounting/settle.py b/accounting/settle.py new file mode 100644 index 0000000..a4af45e --- /dev/null +++ b/accounting/settle.py @@ -0,0 +1,56 @@ +from .const import * +from .accountingnode import get_accounting_nodes +from .excep import * +from .getaccount import getAccountByName +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools +from appPublic.timeUtils import curDateString +from appPublic.argsConvert import ArgsConvert +from .accounting_config import get_accounting_config, AccountingOrgs +from datetime import datetime + +def get_subjectid(salemode): + d = { + '0':'acc009', + '1':'acc010', + '2':'acc011' + } + return d.get(salemode) + +class SettleAccounting: + def __init__(self, settle_log): + self.accounting_orgid = settle_log['accounting_orgid'] + self.settle_log = settle_log + self.providerid = settle_log['providerid'] + self.orderid = None + self.sale_mode = settle_log['sale_mode'] + self.curdate = settle_log['settle_date'] + self.transamount = settle_log['settle_amt'] + self.timestamp = datetime.now() + self.productid = None + self.action = settle_log['business_op'] + self.summary = self.action + self.settleid = getID() + self.billid = getID() + self.bill = { + 'id':self.billid, + 'business_op':self.action, + 'amount':self.transamount, + 'bill_date':self.curdate, + 'bill_timestamp':self.timestamp + } + + async def accounting(self, sor): + ao = AccountingOrgs(self, self.accounting_orgid, None) + await self.write_settle_log(sor) + await self.write_bill(sor) + await ao.do_accounting(sor) + return True + + async def write_settle_log(self, sor): + ns = self.settle_log.copy() + ns['id'] = self.settleid + await sor.C('settle_log', ns) + + async def write_bill(self, sor): + await sor.C('bill', self.bill.copy()) diff --git a/accounting/settledate.py b/accounting/settledate.py new file mode 100644 index 0000000..4844ad7 --- /dev/null +++ b/accounting/settledate.py @@ -0,0 +1,28 @@ +from appPublic.timeUtils import is_match_pattern +from sqlor.sor import SQLor +from sqlor.dbpools import DBPools +from accounting.const import * + + + + + +async def is_provider_settle_date(strdate:str, + providerid:str, + sor:SQLor=None) -> bool: + async def f(sor:SQLor, strdate:str, providerid:str): + sql = """select * from provider where orgid=${providerid}$""" + recs = await sor.sqlExe(sql, {'providerid':providerid}) + if len(recs) == 0: + return False + pattern = recs[0]['settle_datep'] + if pattern is None: + return False + return is_match_pattern(pattern, strdate) + + if sor: + return await f(sor, strdate, providerid) + db = DBPools() + async with db.sqlorContext(DBNAME()) as sor: + return await f(sor, strdate, providerid) + diff --git a/accounting/version.py b/accounting/version.py new file mode 100644 index 0000000..2247efc --- /dev/null +++ b/accounting/version.py @@ -0,0 +1,2 @@ +__version__ = '0.1.0' + diff --git a/app/acc.py b/app/acc.py new file mode 100644 index 0000000..efe84c0 --- /dev/null +++ b/app/acc.py @@ -0,0 +1,29 @@ +import json +import os +from time import time + +from appPublic.worker import awaitify +from appPublic.jsonConfig import getConfig +from ahserver.serverenv import ServerEnv +from appPublic.registerfunction import RegisterFunction +from ahserver.webapp import webapp +from accounting.openaccount import openAllResellerAccounts +from accounting.openaccount import openAllCustomerAccounts +from accounting.openaccount import openAllProviderAccounts +from accounting.openaccount import openRetailRelationshipAccounts +from accounting.openaccount import openOwnerAccounts +from accounting.recharge import recharge_accounting +from accounting.getaccount import getAccountBalance, getCustomerBalance +from accounting.init import load_accounting + +def get_db(module=''): + return 'test' + +def init_func(): + rf = RegisterFunction() + rf.register('get_module_dbname', get_db) + load_accounting() + +if __name__ == '__main__': + webapp(init_func) + diff --git a/docs/平台类系统记账子系统.docx b/docs/平台类系统记账子系统.docx new file mode 100644 index 0000000..8c1cacc Binary files /dev/null and b/docs/平台类系统记账子系统.docx differ diff --git a/json/acc_balance.json b/json/acc_balance.json new file mode 100644 index 0000000..a6d3fab --- /dev/null +++ b/json/acc_balance.json @@ -0,0 +1,17 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/account", + "dbname": "sage", + "tblname": "acc_balance", + "title":"科目", + "params": { + "sortby":"name", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ] + } +} diff --git a/json/acc_detail.json b/json/acc_detail.json new file mode 100644 index 0000000..1bab98e --- /dev/null +++ b/json/acc_detail.json @@ -0,0 +1,17 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/acc_detail", + "dbname": "sage", + "tblname": "acc_detail", + "title":"科目", + "params": { + "sortby":"name", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ] + } +} diff --git a/json/account.json b/json/account.json new file mode 100644 index 0000000..71a9a53 --- /dev/null +++ b/json/account.json @@ -0,0 +1,34 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/account", + "dbname": "sage", + "tblname": "account", + "title":"科目", + "params": { + "sortby":"name", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ], + "subtables":[ + { + "field":"accountid", + "title":"账户余额", + "subtable":"acc_balance" + }, + { + "field":"accountid", + "title":"账户明细", + "subtable":"acc_detail" + }, + { + "field":"accountid", + "title":"账户日志", + "subtable":"accounting_log" + } + ] + } +} diff --git a/json/account_config.json b/json/account_config.json new file mode 100644 index 0000000..fa5d29c --- /dev/null +++ b/json/account_config.json @@ -0,0 +1,16 @@ +{ + "models_dir": "${HOME}$/py/accounting/models", + "output_dir": "${HOME}$/py/sage/wwwroot/account_config", + "dbname": "sage", + "tblname": "account_config", + "title":"账户设置", + "params": { + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ] + } +} diff --git a/json/accounting_config.json b/json/accounting_config.json new file mode 100644 index 0000000..ed13af3 --- /dev/null +++ b/json/accounting_config.json @@ -0,0 +1,14 @@ +{ + "tblname": "accounting_config", + "title":"账务设置", + "params": { + "sortby":["action","accounting_dir"], + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ] + } +} diff --git a/json/accounting_log.json b/json/accounting_log.json new file mode 100644 index 0000000..3d5441f --- /dev/null +++ b/json/accounting_log.json @@ -0,0 +1,17 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/accounting_log", + "dbname": "sage", + "tblname": "accounting_log", + "title":"科目", + "params": { + "sortby":"name", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ] + } +} diff --git a/json/build.sh b/json/build.sh new file mode 100755 index 0000000..2c4a9d1 --- /dev/null +++ b/json/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/bash + +xls2ui -m ../models -o ../wwwroot accounting *.json diff --git a/json/subject.json b/json/subject.json new file mode 100644 index 0000000..71a7c41 --- /dev/null +++ b/json/subject.json @@ -0,0 +1,25 @@ +{ + "models_dir": "${HOME}$/py/rbac/models", + "output_dir": "${HOME}$/py/sage/wwwroot/subject", + "dbname": "sage", + "tblname": "subject", + "title":"科目", + "params": { + "sortby":"name", + "browserfields": { + "exclouded": ["id"], + "cwidth": {} + }, + "editexclouded": [ + "id" + ], + "subtables":[ + { + "field":"subjectid", + "title":"账户设置", + "url":"../account_config", + "subtable":"account_config" + } + ] + } +} diff --git a/models/acc_balance.xlsx b/models/acc_balance.xlsx new file mode 100644 index 0000000..8b4cd3a Binary files /dev/null and b/models/acc_balance.xlsx differ diff --git a/models/acc_detail.xlsx b/models/acc_detail.xlsx new file mode 100644 index 0000000..a3731f7 Binary files /dev/null and b/models/acc_detail.xlsx differ diff --git a/models/account.xlsx b/models/account.xlsx new file mode 100644 index 0000000..17aee05 Binary files /dev/null and b/models/account.xlsx differ diff --git a/models/account_config.xlsx b/models/account_config.xlsx new file mode 100644 index 0000000..ef5b18e Binary files /dev/null and b/models/account_config.xlsx differ diff --git a/models/accounting_config.xlsx b/models/accounting_config.xlsx new file mode 100644 index 0000000..dfdb6b8 Binary files /dev/null and b/models/accounting_config.xlsx differ diff --git a/models/accounting_log.xlsx b/models/accounting_log.xlsx new file mode 100644 index 0000000..bef2b80 Binary files /dev/null and b/models/accounting_log.xlsx differ diff --git a/models/bill.xlsx b/models/bill.xlsx new file mode 100644 index 0000000..6ba50f1 Binary files /dev/null and b/models/bill.xlsx differ diff --git a/models/bill_detail.xlsx b/models/bill_detail.xlsx new file mode 100644 index 0000000..171f690 Binary files /dev/null and b/models/bill_detail.xlsx differ diff --git a/models/ledger.xlsx b/models/ledger.xlsx new file mode 100644 index 0000000..955d4bf Binary files /dev/null and b/models/ledger.xlsx differ diff --git a/models/subject.xlsx b/models/subject.xlsx new file mode 100644 index 0000000..5406d6d Binary files /dev/null and b/models/subject.xlsx differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..05a1a48 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# git+https://git.kaiyuancloud.cn/yumoqing/rbac + diff --git a/script/roleperm.sh b/script/roleperm.sh new file mode 100755 index 0000000..1923f6e --- /dev/null +++ b/script/roleperm.sh @@ -0,0 +1,8 @@ +#!/usr/bin/bash + +python ~/py/rbac/script/roleperm.py sage accounting owner superuser account_config accounting_config subject +python ~/py/rbac/script/roleperm.py sage accounting reseller operator account acc_detail acc_balance accounting_log bill bill_detail ledger +python ~/py/rbac/script/roleperm.py sage accounting reseller sale account acc_detail acc_balance accounting_log bill bill_detail ledger +python ~/py/rbac/script/roleperm.py sage accounting reseller accountant account acc_detail acc_balance accounting_log bill bill_detail ledger +python ~/py/rbac/script/roleperm.py sage accounting customer customer account acc_detail acc_balance accounting_log bill bill_detail ledger + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..2f9af9e --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from accounting.version import __version__ +try: + from setuptools import setup +except ImportError: + from distutils.core import setup +required = [] +with open('requirements.txt', 'r') as f: + ls = f.read() + required = ls.split('\n') + +with open('accounting/version.py', 'r') as f: + x = f.read() + y = x[x.index("'")+1:] + z = y[:y.index("'")] + version = z +with open("README.md", "r") as fh: + long_description = fh.read() + +name = "accounting" +description = "accounting" +author = "yumoqing" +email = "yumoqing@gmail.com" + +package_data = {} + +setup( + name="accounting", + version=version, + + # uncomment the following lines if you fill them out in release.py + description=description, + author=author, + author_email=email, + platforms='any', + install_requires=required , + packages=[ + "accounting" + ], + package_data=package_data, + keywords = [ + ], + url="https://github.com/yumoqing/accounting", + long_description=long_description, + long_description_content_type="text/markdown", + classifiers = [ + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License', + ], +) diff --git a/test/open_account.py b/test/open_account.py new file mode 100644 index 0000000..4e7ecf0 --- /dev/null +++ b/test/open_account.py @@ -0,0 +1,31 @@ +from run_test import run +from appPublic.log import debug +from sqlor.dbpools import DBPools +from accounting.openaccount import openAllCustomerAccounts, \ + openOwnerAccounts, openAllProviderAccounts, openAllResellerAccounts + +import test_rf + +async def OpenAccount(): + orgid = '_VaV5trl8faujgr7xaE3D' + db = DBPools() + async with db.sqlorContext('sage') as sor: + try: + await openAllCustomerAccounts(sor, orgid) + except Exception as e: + debug(f'{e}') + try: + await openOwnerAccounts(sor, orgid) + except Exception as e: + debug(f'{e}') + try: + await openAllProviderAccounts(sor, orgid) + except Exception as e: + debug(f'{e}') + try: + await openAllResellerAccounts(sor, orgid) + except Exception as e: + debug(f'{e}') + +if __name__ == '__main__': + run(OpenAccount) diff --git a/test/recharge.py b/test/recharge.py new file mode 100644 index 0000000..3e5213c --- /dev/null +++ b/test/recharge.py @@ -0,0 +1,47 @@ +import asyncio +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig +from appPublic.dictObject import DictObject +from accounting.recharge import recharge_accounting + +async def test1(): + db = DBPools() + dbname = 'sage' + async with db.sqlorContext(dbname) as sor: + await recharge_accounting(sor, '4twr0MGHMZ4aFHYzQ9Lxh', 'RECHARGE', 'oEdlkg4SfX6JH5xQnCHW', '2025-01-10', 100) + +async def test2(): + rl = DictObject() + rl.customerid = '4zXVMkBCEaTmR0xwneUBX' + rl.recharge_date = '2024-09-21' + rl.recharge_amt = 100 + rl.action = 'RECHARGE_REVERSE' + rl.orderid = '1' + rl.recharge_channel = 'alipay' + ra = RechargeAccounting(rl) + db = DBPools() + async with db.sqlorContext('sage') as sor: + await ra.accounting(sor) + +async def test(): + await test1() + await test2() + +if __name__ == '__main__': + DBPools({ + "sage":{ + "driver":"aiomysql", + "async_mode":True, + "coding":"utf8", + "maxconn":100, + "dbname":"sage", + "kwargs":{ + "user":"test", + "db":"sage", + "password":"QUZVcXg5V1p1STMybG5Ia6mX9D0v7+g=", + "host":"localhost" + } + } + }) + asyncio.get_event_loop().run_until_complete(test1()) + diff --git a/test/run_test.py b/test/run_test.py new file mode 100644 index 0000000..b72908d --- /dev/null +++ b/test/run_test.py @@ -0,0 +1,12 @@ +import os +import asyncio + +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig + +def run(test_coro): + os.chdir('/Users/ymq/py/sage') + conf = getConfig() + DBPools(conf.databases) + asyncio.get_event_loop().run_until_complete(test_coro()) + diff --git a/test/test_rf.py b/test/test_rf.py new file mode 100644 index 0000000..bd9e058 --- /dev/null +++ b/test/test_rf.py @@ -0,0 +1,45 @@ +from appPublic.registerfunction import RegisterFunction + +rf = RegisterFunction() + +def get_module_database(module_name): + return 'sage' + +rf.register('get_module_database', get_module_database) + +async def get_providers_by_orgid(sor, accounting_orgid): + sql = """select * from organization a, orgtypes b +where a.parentid = ${accounting_orgid}$ and + b.orgtypeid = 'provider' and + a.id = b.orgid """ + recs = await sor.sqlExe(sql, {'accounting_orgid':accounting_orgid}) + return recs + +rf.register('get_providers_by_orgid', get_providers_by_orgid) + +async def get_resellers_by_orgid(sor, accounting_orgid): + sql = """select * from organization a, orgtypes b +where a.parentid=${accounting_orgid}$ and + a.id = b.orgid and + b.orgtypeid = 'reseller'""" + recs = await sor.sqlExe(sql, {'accounting_orgid':accounting_orgid}) + return recs + +rf.register('get_resellers_by_orgid', get_resellers_by_orgid) + +async def get_customers_by_orgid(sor, accounting_orgid): + sql = """select * from organization a, + (select orgid, count(*) cnt + from orgtypes + where orgtypeid in ('customer', 'personalcustomer', 'agencycustomer') + group by orgid + ) b +where a.parentid=${accounting_orgid}$ and + a.id = b.orgid and + b.cnt > 0 + """ + recs = await sor.sqlExe(sql, {'accounting_orgid':accounting_orgid}) + return recs + +rf.register('get_customers_by_orgid', get_customers_by_orgid) +