first commit

This commit is contained in:
yumoqing 2025-07-16 14:32:14 +08:00
commit 6faf88b224
50 changed files with 2072 additions and 0 deletions

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# accounting

0
accounting/__init__.py Normal file
View File

View File

@ -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(legaccounting_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())

View File

@ -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

View File

@ -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

95
accounting/argsconvert.py Normal file
View File

@ -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)

123
accounting/bill.py Normal file
View File

@ -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

262
accounting/bizaccount.py Normal file
View File

@ -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())

53
accounting/bzdate.py Normal file
View File

@ -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

44
accounting/const.py Normal file
View File

@ -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'

63
accounting/consume.py Normal file
View File

@ -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())

View File

@ -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)

121
accounting/excep.py Normal file
View File

@ -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)

156
accounting/getaccount.py Normal file
View File

@ -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

7
accounting/getdbname.py Normal file
View File

@ -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')

23
accounting/init.py Normal file
View File

@ -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

28
accounting/ledger.py Normal file
View File

@ -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())

101
accounting/openaccount.py Normal file
View File

@ -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)

View File

@ -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)

87
accounting/recharge.py Normal file
View File

@ -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

56
accounting/settle.py Normal file
View File

@ -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())

28
accounting/settledate.py Normal file
View File

@ -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)

2
accounting/version.py Normal file
View File

@ -0,0 +1,2 @@
__version__ = '0.1.0'

29
app/acc.py Normal file
View File

@ -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)

Binary file not shown.

17
json/acc_balance.json Normal file
View File

@ -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"
]
}
}

17
json/acc_detail.json Normal file
View File

@ -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"
]
}
}

34
json/account.json Normal file
View File

@ -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"
}
]
}
}

16
json/account_config.json Normal file
View File

@ -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"
]
}
}

View File

@ -0,0 +1,14 @@
{
"tblname": "accounting_config",
"title":"账务设置",
"params": {
"sortby":["action","accounting_dir"],
"browserfields": {
"exclouded": ["id"],
"cwidth": {}
},
"editexclouded": [
"id"
]
}
}

17
json/accounting_log.json Normal file
View File

@ -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"
]
}
}

3
json/build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/bash
xls2ui -m ../models -o ../wwwroot accounting *.json

25
json/subject.json Normal file
View File

@ -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"
}
]
}
}

BIN
models/acc_balance.xlsx Normal file

Binary file not shown.

BIN
models/acc_detail.xlsx Normal file

Binary file not shown.

BIN
models/account.xlsx Normal file

Binary file not shown.

BIN
models/account_config.xlsx Normal file

Binary file not shown.

Binary file not shown.

BIN
models/accounting_log.xlsx Normal file

Binary file not shown.

BIN
models/bill.xlsx Normal file

Binary file not shown.

BIN
models/bill_detail.xlsx Normal file

Binary file not shown.

BIN
models/ledger.xlsx Normal file

Binary file not shown.

BIN
models/subject.xlsx Normal file

Binary file not shown.

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
# git+https://git.kaiyuancloud.cn/yumoqing/rbac

8
script/roleperm.sh Executable file
View File

@ -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

52
setup.py Executable file
View File

@ -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',
],
)

31
test/open_account.py Normal file
View File

@ -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)

47
test/recharge.py Normal file
View File

@ -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())

12
test/run_test.py Normal file
View File

@ -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())

45
test/test_rf.py Normal file
View File

@ -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)