456 lines
14 KiB
Python
456 lines
14 KiB
Python
import asyncio
|
|
import re
|
|
from .const import *
|
|
from .accountingnode import get_parent_orgid
|
|
from .excep import *
|
|
from .getaccount import getAccountByName
|
|
from appPublic.uniqueID import getID
|
|
from appPublic.log import debug
|
|
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 AccountingOrgs:
|
|
def __init__(self, caller,
|
|
accounting_orgid,
|
|
customerid,
|
|
resellerid=None
|
|
):
|
|
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 = self.action
|
|
self.providerid = caller.providerid
|
|
self.productid = caller.productid
|
|
self.accounting_orgid = accounting_orgid
|
|
self.resellerid = resellerid
|
|
self.customerid = customerid
|
|
self.own_salemode = None
|
|
self.reseller_salemode = None
|
|
self.variable = {
|
|
'交易金额':self.caller.transamount
|
|
}
|
|
self.salemode_sql0 = """
|
|
select a.*, b.providerid, b.productid, b.discount, b.price
|
|
from saleprotocol a, product_salemode b
|
|
where a.id = b.protocolid
|
|
and a.bid_orgid=${bid_orgid}$
|
|
and (b.productid=${productid}$ or b.productid = '*')
|
|
and b.providerid = ${providerid}$
|
|
and a.start_date <= ${curdate}$
|
|
and a.end_date > ${curdate}$
|
|
order by productid desc
|
|
"""
|
|
self.salemode_sql = """
|
|
select a.*, b.providerid, b.productid, b.discount, b.price
|
|
from saleprotocol a, product_salemode b
|
|
where a.id = b.protocolid
|
|
and a.offer_orgid=${offer_orgid}$
|
|
and b.providerid = ${providerid}$
|
|
and a.bid_orgid=${bid_orgid}$
|
|
and (b.productid=${productid}$ or b.productid = '*')
|
|
and a.start_date <= ${curdate}$
|
|
and a.end_date > ${curdate}$
|
|
order by productid desc
|
|
"""
|
|
|
|
async def is_business_owner(self):
|
|
sor = self.sor
|
|
recs = await sor.sqlExe("select * from organization where id=${orgid}$ and parentid is null",
|
|
{'orgid':self.accounting_orgid})
|
|
if len(recs) > 0:
|
|
return True
|
|
return False
|
|
|
|
async def check_add_realtime_settle_legs(self):
|
|
sor = self.sor
|
|
if self.settle_mode == '0':
|
|
await self.add_online_settle_legs()
|
|
print('settle legs added ....')
|
|
else:
|
|
print(f'{self.providerid=}')
|
|
|
|
async def add_online_settle_legs(self):
|
|
specstr = ACTNAME_SETTLE + '-' + self.own_salemode + '-实时'
|
|
ls = [r.copy() for r in accounting_config if r['specstr'] == specstr ]
|
|
for l in ls:
|
|
if self.action.endswith('_REVERSE'):
|
|
l['summary'] = 'SETTLE_REVERSE'
|
|
else:
|
|
l['summary'] = 'SETTLE'
|
|
|
|
self.accounting_legs += ls
|
|
self.realtimesettled = True
|
|
|
|
async def setup_accounting_legs(self):
|
|
global accounting_config
|
|
specstr = await self.get_act_specstr()
|
|
self.specstr = specstr
|
|
await get_accounting_config(self.sor)
|
|
aorgtype = '客户所在机构'
|
|
if self.resellerid:
|
|
aorgtype = '分销商机构'
|
|
debug(f'{accounting_config=}, {aorgtype=}')
|
|
if self.specstr.startswith(ACTNAME_SETTLE):
|
|
self.accounting_legs = [r.copy() for r in accounting_config
|
|
if r['specstr'] == specstr ]
|
|
else:
|
|
self.accounting_legs = [r.copy() for r in accounting_config
|
|
if r['specstr'] == specstr
|
|
and r['accounting_orgtype'] == aorgtype]
|
|
debug(f'{self.accounting_legs=}')
|
|
for l in self.accounting_legs:
|
|
l['summary'] = self.action
|
|
|
|
if self.specstr.startswith(ACTNAME_BUY):
|
|
await self.check_add_realtime_settle_legs()
|
|
else:
|
|
print(f'{self.specstr} is notstartswith {ACTNAME_BUY}')
|
|
|
|
print(f'setup_accounting_legs():{self.specstr}')
|
|
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('${', '}$')
|
|
print(f'{l["id"]},{l["amt_pattern"]=}')
|
|
try:
|
|
l['amount'] = eval(await ac.convert(
|
|
l['amt_pattern'],
|
|
self.variable.copy(),
|
|
default=self.localamount))
|
|
except Exception as e:
|
|
print(l['amt_pattern'], l['id'], self.variable)
|
|
raise e
|
|
|
|
if l['amount'] is None:
|
|
print(f'amount is None:{l["amt_pattern"]}, {self.variable=},{self.caller.bill=}')
|
|
raise AccountingAmountIsNone(self.caller.billid)
|
|
|
|
async def setup_bill_variable(self):
|
|
"""
|
|
'本方折扣'
|
|
'客户折扣'
|
|
'分销商折扣'
|
|
'进价'
|
|
'客户售价'
|
|
'分销商售价'
|
|
"""
|
|
sor = self.sor
|
|
recs = await sor.sqlExe(self.salemode_sql0, {
|
|
'bid_orgid':self.accounting_orgid,
|
|
'providerid':self.providerid,
|
|
'productid':self.productid,
|
|
'curdate':self.curdate})
|
|
if len(recs) == 0:
|
|
raise ProductBidProtocolNotDefined(None, self.accounting_orgid,
|
|
self.providerid,
|
|
self.productid,
|
|
self.curdate
|
|
)
|
|
rec = recs[0]
|
|
self.settle_mode = rec['settle_mode']
|
|
self.quantity = self.caller.bill['quantity']
|
|
salemode=rec['salemode']
|
|
if salemode == '0':
|
|
self.variable['本方折扣'] = rec['discount']
|
|
elif salemode == '2':
|
|
self.variable['进价'] = rec['price'] * self.quantity
|
|
|
|
recs = await sor.sqlExe(self.salemode_sql, {
|
|
'offer_orgid':self.accounting_orgid,
|
|
'bid_orgid':self.customerid,
|
|
'providerid':self.providerid,
|
|
'productid':self.productid,
|
|
'curdate':self.curdate})
|
|
if len(recs) == 0:
|
|
recs = await sor.sqlExe(self.salemode_sql, {
|
|
'offer_orgid':self.accounting_orgid,
|
|
'bid_orgid':'*',
|
|
'providerid':self.providerid,
|
|
'productid':self.productid,
|
|
'curdate':self.curdate})
|
|
print(f'get customer price or discount, {recs=}')
|
|
if len(recs) == 0:
|
|
raise ProductBidProtocolNotDefined(None, self.customerid,
|
|
self.providerid,
|
|
self.productid,
|
|
self.curdate
|
|
)
|
|
rec = recs[0]
|
|
salemode=rec['salemode']
|
|
if salemode == '0':
|
|
self.variable['客户折扣'] = rec['discount']
|
|
elif salemode == '2':
|
|
self.variable['客户售价'] = rec['price'] * self.quantity
|
|
|
|
if self.resellerid:
|
|
recs = await sor.sqlExe(self.salemode_sql, {
|
|
'offer_orgid':self.accounting_orgid,
|
|
'bid_orgid':self.resellerid,
|
|
'providerid':self.providerid,
|
|
'productid':self.productid,
|
|
'curdate':self.curdate})
|
|
if len(recs) == 0:
|
|
raise ProductBidProtocolNotDefined(None, self.resellerid,
|
|
self.providerid,
|
|
self.productid,
|
|
self.curdate
|
|
)
|
|
rec = recs[0]
|
|
salemode=rec['salemode']
|
|
if salemode == '0':
|
|
self.variable['分销商折扣'] = rec['discount']
|
|
elif salemode == '2':
|
|
self.variable['分销商售价'] = rec['price'] * self.quantity
|
|
|
|
async def localamount(self, name):
|
|
a = name.split('-')
|
|
if len(a) == 3:
|
|
for l in self.accounting_legs:
|
|
if a[0] == l['accounting_dir'] and \
|
|
a[1] == l['orgtype'] and \
|
|
a[2] == l['subjectname']:
|
|
return l['amount']
|
|
if name[0] == '#':
|
|
i = int(name[1:])
|
|
return self.accounting_legs[i]['amount']
|
|
|
|
print(f'{name} not found')
|
|
|
|
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 += legs['amount']
|
|
else:
|
|
credit_balance += legs['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()
|
|
print('do_accounting() ...', self.accounting_legs)
|
|
self.check_accounting_balance(self.accounting_legs)
|
|
for leg in self.accounting_legs:
|
|
orgid = self.accounting_orgid
|
|
if leg['orgtype'] == '客户':
|
|
orgid = self.customerid
|
|
elif leg['orgtype'] == '分销商':
|
|
orgid = self.resellerid
|
|
elif leg['orgtype'] == '供应商':
|
|
orgid = self.providerid
|
|
accid = await getAccountByName(sor,
|
|
self.accounting_orgid,
|
|
orgid,
|
|
leg['subjectname'])
|
|
if accid is None:
|
|
print('can not get accountid', self.accounting_orgid, orgid, leg['subjectname'], leg['id'])
|
|
raise AccountIdNone(self.accounting_orgid, orgid, leg['subjectname'])
|
|
leg['orgid'] = orgid
|
|
await self.leg_accounting(sor, accid, leg)
|
|
if self.realtimesettled:
|
|
x = await self.is_business_owner()
|
|
if x:
|
|
await self.write_settle_log()
|
|
|
|
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' : self.accounting_orgid,
|
|
'billid' : self.billid,
|
|
'description' : self.specstr,
|
|
'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':leg['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':leg['summary'],
|
|
'amount':leg['amount'],
|
|
'balance':leg['new_balance'],
|
|
'acclogid':logid
|
|
}
|
|
await sor.C('acc_detail', ns.copy())
|
|
|
|
async def get_reseller_salemode(self, orgid):
|
|
sor = self.sor
|
|
recs = await sor.sqlExe(self.salemode_sql0,
|
|
{
|
|
'bid_orgid':orgid,
|
|
'providerid':self.providerid,
|
|
'productid':self.productid,
|
|
'curdate':self.curdate
|
|
})
|
|
if len(recs) == 0:
|
|
return None
|
|
return recs[0]['salemode']
|
|
|
|
async def get_act_specstr(self):
|
|
sor = self.sor
|
|
debug(f'{self.action=} ---')
|
|
if self.action in [ ACTION_RECHARGE, ACTION_RECHARGE_REVERSE ]:
|
|
debug(f'{self.action=}, {ACTNAME_RECHARGE=}')
|
|
return ACTNAME_RECHARGE
|
|
|
|
if self.action in [ ACTION_SETTLE, ACTION_SETTLE_REVERSE ]:
|
|
spec = ACTNAME_SETTLE
|
|
if self.caller.sale_mode == '0':
|
|
spec = f'{ACTNAME_SETTLE}-{SALEMODE_DISCOUNT}'
|
|
elif self.caller.sale_mode == '1':
|
|
spec = f'{ACTNAME_SETTLE}-{SALEMODE_REBATE}'
|
|
else:
|
|
spec = f'{ACTNAME_SETTLE}-{SALEMODE_FLOORPRICE}'
|
|
debug(f'{self.action=} {spec=}')
|
|
return spec
|
|
|
|
|
|
ret = ACTNAME_BUY
|
|
for id in [self.accounting_orgid, self.resellerid]:
|
|
if id is None:
|
|
break
|
|
salemode = await self.get_reseller_salemode(id)
|
|
if salemode == '0':
|
|
sale_mode = SALEMODE_DISCOUNT
|
|
elif salemode == '1':
|
|
sale_mode = SALEMODE_REBATE
|
|
else:
|
|
sale_mode = SALEMODE_FLOORPRICE
|
|
|
|
ret += '-' + sale_mode
|
|
if id == self.accounting_orgid:
|
|
self.own_salemode = sale_mode
|
|
else:
|
|
self.reseller_salemode = sale_mode
|
|
await self.setup_bill_variable()
|
|
debug(f'{ret=}')
|
|
return ret
|
|
|
|
|