kboss/b/bill/finance_settlement_apv_callback.dspy
2026-06-18 17:48:01 +08:00

302 lines
9.4 KiB
Plaintext

# -*- coding: utf-8 -*-
"""财务结算审批回调。"""
import datetime
DBNAME = 'kboss'
def _period_settle_mode(period_type):
if period_type == 'day':
return '1'
if period_type == 'month':
return '3'
return '0'
async def _business_date():
try:
return await get_business_date(sor=None)
except Exception:
return datetime.datetime.now().strftime('%Y-%m-%d')
async def _settle_supplier(sor, settlement):
rows = await sor.sqlExe("""
SELECT sale_mode, SUM(settlement_amount) AS amount
FROM finance_settlement_detail
WHERE settlement_id=${settlement_id}$
AND del_flg='0'
GROUP BY sale_mode
""", {'settlement_id': settlement['id']})
settle_date = await _business_date()
settle_mode = _period_settle_mode(settlement.get('period_type'))
failures = []
for row in rows:
amount = round(float(row.get('amount') or 0), 8)
if amount == 0:
continue
sale_mode = row.get('sale_mode') or '0'
try:
settle_log = {
'accounting_orgid': settlement.get('accounting_orgid'),
'providerid': settlement.get('counterparty_orgid'),
'settle_date': settle_date,
'settle_mode': settle_mode,
'sale_mode': sale_mode,
'settle_amt': amount,
'business_op': 'SETTLE',
}
ai = SettleAccounting(settle_log)
await ai.accounting(sor)
except Exception as e:
failures.append('sale_mode=%s: %s' % (sale_mode, str(e)))
if failures:
return False, '; '.join(failures)
return True, None
async def _get_account(sor, accounting_orgid, orgid, subjectname):
rows = await sor.sqlExe("""
SELECT a.*
FROM account a
INNER JOIN subject s ON a.subjectid=s.id
WHERE a.accounting_orgid=${accounting_orgid}$
AND a.orgid=${orgid}$
AND s.name=${subjectname}$
AND a.del_flg='0'
AND s.del_flg='0'
LIMIT 1
""", {
'accounting_orgid': accounting_orgid,
'orgid': orgid,
'subjectname': subjectname,
})
return rows[0] if rows else None
async def _latest_balance(sor, accountid):
rows = await sor.sqlExe("""
SELECT balance
FROM acc_balance
WHERE accountid=${accountid}$ AND del_flg='0'
ORDER BY acc_date DESC
LIMIT 1
""", {'accountid': accountid})
if not rows or rows[0].get('balance') is None:
return 0.0
return float(rows[0].get('balance') or 0)
def _next_balance(account, balance, acc_dir, amount):
balance_at = account.get('balance_at')
if (
(balance_at == '0' and acc_dir == '1')
or (balance_at == '1' and acc_dir == '0')
):
return balance - amount
return balance + amount
async def _write_balance(sor, accountid, acc_date, balance):
rows = await sor.sqlExe("""
SELECT id
FROM acc_balance
WHERE accountid=${accountid}$ AND acc_date=${acc_date}$ AND del_flg='0'
LIMIT 1
""", {'accountid': accountid, 'acc_date': acc_date})
if rows:
await sor.U('acc_balance', {
'id': rows[0]['id'],
'balance': balance,
})
else:
await sor.C('acc_balance', {
'id': uuid(),
'accountid': accountid,
'acc_date': acc_date,
'balance': balance,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
async def _write_reseller_leg(
sor,
billid,
acc_date,
accounting_orgid,
participantid,
participanttype,
subjectname,
accounting_dir,
amount,
description,
):
account = await _get_account(sor, accounting_orgid, participantid, subjectname)
if not account:
raise Exception(
'未找到账户 accounting_orgid=%s, orgid=%s, subject=%s'
% (accounting_orgid, participantid, subjectname)
)
acc_dir = '0' if accounting_dir == '借' else '1'
old_balance = await _latest_balance(sor, account['id'])
new_balance = _next_balance(account, old_balance, acc_dir, amount)
await _write_balance(sor, account['id'], acc_date, new_balance)
await sor.C('bill_detail', {
'id': uuid(),
'accounting_orgid': accounting_orgid,
'billid': billid,
'description': description,
'participantid': participantid,
'participanttype': participanttype,
'subjectname': subjectname,
'accounting_dir': accounting_dir,
'amount': amount,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
logid = uuid()
await sor.C('accounting_log', {
'id': logid,
'accountid': account['id'],
'acc_date': acc_date,
'acc_timestamp': datetime.datetime.now(),
'acc_dir': acc_dir,
'summary': 'SETTLE',
'amount': amount,
'billid': billid,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
await sor.C('acc_detail', {
'id': uuid(),
'accountid': account['id'],
'acc_date': acc_date,
'acc_timestamp': datetime.datetime.now(),
'acc_dir': acc_dir,
'summary': 'SETTLE',
'amount': amount,
'balance': new_balance,
'acclogid': logid,
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
async def _settle_reseller(sor, settlement):
amount = round(float(settlement.get('settlement_amount') or 0), 8)
if amount == 0:
return True, None
if amount < 0:
return False, '分销商结算金额为负数,需先人工确认红冲口径'
accounting_orgid = settlement.get('accounting_orgid')
reseller_orgid = settlement.get('counterparty_orgid')
settle_date = await _business_date()
billid = uuid()
description = '分销商结算-%s' % settlement.get('settlement_no')
await sor.C('bill', {
'id': billid,
'customerid': reseller_orgid,
'business_op': 'SETTLE',
'amount': amount,
'bill_date': settle_date,
'bill_timestamp': datetime.datetime.now(),
'bill_state': '1',
'del_flg': '0',
'create_at': datetime.datetime.now(),
})
# 结算分录:借记分销商存放资金,贷记本机构资金账号。
await _write_reseller_leg(
sor,
billid,
settle_date,
accounting_orgid,
reseller_orgid,
'分销商',
'分销商存放资金',
'借',
amount,
description,
)
await _write_reseller_leg(
sor,
billid,
settle_date,
accounting_orgid,
accounting_orgid,
'本机构',
'资金账号',
'贷',
amount,
description,
)
return True, None
async def finance_settlement_apv_callback(ns={}):
approval_id = ns.get('approval_id') or ns.get('apv_id')
status = ns.get('status')
if not approval_id or not status:
return {'status': False, 'msg': '缺少 approval_id/apv_id 或 status'}
status_map = {
'start': 'approving',
'agree': 'approved',
'refuse': 'rejected',
'terminate': 'cancelled',
}
next_status = status_map.get(status, status)
db = DBPools()
async with db.sqlorContext(DBNAME) as sor:
try:
rows = await sor.R('finance_settlement', {'approval_id': approval_id, 'del_flg': '0'})
if not rows:
return {'status': False, 'msg': '未找到结算审批数据'}
now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
results = []
for settlement in rows:
update_data = {
'id': settlement['id'],
'approval_status': status,
'status': next_status,
'update_at': now_time,
}
if status != 'agree':
await sor.U('finance_settlement', update_data)
results.append({'settlement_id': settlement['id'], 'status': next_status})
continue
if settlement.get('status') == 'settled':
results.append({'settlement_id': settlement['id'], 'status': 'settled'})
continue
if settlement.get('counterparty_type') == 'supplier':
ok, err = await _settle_supplier(sor, settlement)
else:
ok, err = await _settle_reseller(sor, settlement)
if ok:
update_data['status'] = 'settled'
update_data['settled_at'] = now_time
update_data['failure_reason'] = None
results.append({'settlement_id': settlement['id'], 'status': 'settled'})
else:
update_data['status'] = 'failed'
update_data['failure_reason'] = err
results.append({
'settlement_id': settlement['id'],
'status': 'failed',
'failure_reason': err,
})
await sor.U('finance_settlement', update_data)
return {'status': True, 'msg': 'ok', 'data': results}
except Exception as e:
return {'status': False, 'msg': '审批回调处理失败, %s' % str(e)}
ret = await finance_settlement_apv_callback(params_kw)
return ret