feat: multi-currency support — currency/exchange_rate tables + migration
This commit is contained in:
parent
b95a57fd34
commit
3f6c71b4d5
@ -40,6 +40,9 @@ class ConsumeBiz(PFBiz):
|
||||
self.providerid = od['providerid']
|
||||
self.summary = f'{self.action}|{self.customerid}|{self.resellerid}|{self.productid}'
|
||||
self.variable = od.variable
|
||||
self.currency = getattr(od, 'currency', 'CNY')
|
||||
transamt = getattr(od, 'transamt', 0)
|
||||
self.base_amount = getattr(od, 'base_amount', transamt)
|
||||
|
||||
async def get_orgid_by_trans_role(self, sor, leg, role):
|
||||
if role == 'owner':
|
||||
|
||||
103
accounting/exchange.py
Normal file
103
accounting/exchange.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""Multi-currency exchange rate lookup and currency conversion."""
|
||||
from datetime import datetime
|
||||
from sqlor.dbpools import get_sor_context
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.log import debug, exception
|
||||
|
||||
# Fixed base currency (本位币)
|
||||
BASE_CURRENCY = 'CNY'
|
||||
|
||||
|
||||
async def get_exchange_rate(from_currency: str, to_currency: str,
|
||||
rate_type: str = 'sell_rate') -> float:
|
||||
"""Get real-time exchange rate for currency pair.
|
||||
|
||||
Args:
|
||||
from_currency: source currency (e.g. 'USD')
|
||||
to_currency: target currency (e.g. 'CNY')
|
||||
rate_type: 'buy_rate' | 'sell_rate' | 'mid_rate'
|
||||
|
||||
Returns:
|
||||
float exchange rate, or 1.0 if same currency
|
||||
"""
|
||||
if from_currency == to_currency:
|
||||
return 1.0
|
||||
|
||||
env = ServerEnv()
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
async with get_sor_context(env, 'accounting') as sor:
|
||||
sql = """SELECT %s FROM exchange_rate
|
||||
WHERE from_currency = ${from_cur}$
|
||||
AND to_currency = ${to_cur}$
|
||||
AND effective_date <= ${today}$
|
||||
ORDER BY effective_date DESC
|
||||
LIMIT 1""" % rate_type
|
||||
ns = {
|
||||
'from_cur': from_currency,
|
||||
'to_cur': to_currency,
|
||||
'today': today,
|
||||
}
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
if recs:
|
||||
rate = getattr(recs[0], rate_type, 1.0)
|
||||
debug(f'exchange_rate: {from_currency}→{to_currency} {rate_type}={rate}')
|
||||
return float(rate)
|
||||
|
||||
# Fallback: try reverse rate
|
||||
reverse_type = 'sell_rate' if rate_type == 'buy_rate' else 'buy_rate' if rate_type == 'sell_rate' else 'mid_rate'
|
||||
async with get_sor_context(env, 'accounting') as sor:
|
||||
sql = """SELECT %s FROM exchange_rate
|
||||
WHERE from_currency = ${to_cur}$
|
||||
AND to_currency = ${from_cur}$
|
||||
AND effective_date <= ${today}$
|
||||
ORDER BY effective_date DESC
|
||||
LIMIT 1""" % reverse_type
|
||||
ns = {
|
||||
'from_cur': to_currency,
|
||||
'to_cur': from_currency,
|
||||
'today': today,
|
||||
}
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
if recs:
|
||||
rate = 1.0 / float(getattr(recs[0], reverse_type, 1.0))
|
||||
debug(f'exchange_rate(reverse): {from_currency}→{to_currency} {rate_type}={rate}')
|
||||
return rate
|
||||
|
||||
exception(f'No exchange rate found: {from_currency}→{to_currency}')
|
||||
return 1.0
|
||||
|
||||
|
||||
async def convert_currency(amount: float, from_currency: str, to_currency: str,
|
||||
rate_type: str = 'sell_rate') -> float:
|
||||
"""Convert amount between currencies at real-time rate."""
|
||||
if from_currency == to_currency:
|
||||
return amount
|
||||
rate = await get_exchange_rate(from_currency, to_currency, rate_type)
|
||||
return round(amount * rate, 2)
|
||||
|
||||
|
||||
async def convert_to_base(amount: float, from_currency: str,
|
||||
rate_type: str = 'sell_rate') -> float:
|
||||
"""Convert amount to base currency (CNY)."""
|
||||
return await convert_currency(amount, from_currency, BASE_CURRENCY, rate_type)
|
||||
|
||||
|
||||
async def get_user_currency(userorgid: str) -> str:
|
||||
"""Get user organization's preferred billing currency.
|
||||
|
||||
Checks account table — prefers consume account's currency.
|
||||
Falls back to CNY.
|
||||
"""
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'accounting') as sor:
|
||||
# Check consume account currency
|
||||
sql = """SELECT a.currency FROM account a
|
||||
JOIN subject s ON a.subjectid = s.id
|
||||
WHERE a.orgid = ${orgid}$ AND s.name = 'consume'
|
||||
LIMIT 1"""
|
||||
recs = await sor.sqlExe(sql, {'orgid': userorgid})
|
||||
if recs:
|
||||
return recs[0].currency or BASE_CURRENCY
|
||||
|
||||
return BASE_CURRENCY
|
||||
@ -11,6 +11,7 @@ from .stats import get_accounting_stats
|
||||
from .recharge import RechargeBiz, recharge_accounting
|
||||
from .consume import consume_accounting
|
||||
from .creditlimit import get_credit_limit_for_account, update_used_credit, set_credit_limit, get_credit_stats, get_my_credit_list, get_all_customer_credits
|
||||
from .exchange import get_exchange_rate, convert_currency, convert_to_base, get_user_currency, BASE_CURRENCY
|
||||
|
||||
async def all_my_accounts(request):
|
||||
env = request._run_ns
|
||||
@ -83,6 +84,11 @@ def load_accounting():
|
||||
g.get_credit_stats_web = get_credit_stats_web
|
||||
g.get_my_credits_web = get_my_credits_web
|
||||
g.get_all_credits_web = get_all_credits_web
|
||||
g.get_exchange_rate = get_exchange_rate
|
||||
g.convert_currency = convert_currency
|
||||
g.convert_to_base = convert_to_base
|
||||
g.get_user_currency = get_user_currency
|
||||
g.BASE_CURRENCY = BASE_CURRENCY
|
||||
|
||||
|
||||
async def get_credit_stats_web(request):
|
||||
|
||||
@ -57,4 +57,68 @@ appcodes_kv:
|
||||
parentid: credit_status
|
||||
k: expired
|
||||
v: 已过期
|
||||
- id: currency_status_active
|
||||
parentid: currency_status
|
||||
k: active
|
||||
v: 启用
|
||||
- id: currency_status_inactive
|
||||
parentid: currency_status
|
||||
k: inactive
|
||||
v: 停用
|
||||
|
||||
currency:
|
||||
- id: CNY
|
||||
name: 人民币
|
||||
symbol: ¥
|
||||
decimal_places: 2
|
||||
is_base: '1'
|
||||
status: active
|
||||
- id: USD
|
||||
name: 美元
|
||||
symbol: $
|
||||
decimal_places: 2
|
||||
is_base: '0'
|
||||
status: active
|
||||
- id: JPY
|
||||
name: 日元
|
||||
symbol: ¥
|
||||
decimal_places: 0
|
||||
is_base: '0'
|
||||
status: active
|
||||
- id: GBP
|
||||
name: 英镑
|
||||
symbol: £
|
||||
decimal_places: 2
|
||||
is_base: '0'
|
||||
status: active
|
||||
|
||||
exchange_rate:
|
||||
- id: er_usd_cny
|
||||
from_currency: USD
|
||||
to_currency: CNY
|
||||
buy_rate: 7.150000
|
||||
sell_rate: 7.250000
|
||||
mid_rate: 7.200000
|
||||
effective_date: '2025-01-01'
|
||||
- id: er_jpy_cny
|
||||
from_currency: JPY
|
||||
to_currency: CNY
|
||||
buy_rate: 0.048000
|
||||
sell_rate: 0.049000
|
||||
mid_rate: 0.048500
|
||||
effective_date: '2025-01-01'
|
||||
- id: er_gbp_cny
|
||||
from_currency: GBP
|
||||
to_currency: CNY
|
||||
buy_rate: 9.100000
|
||||
sell_rate: 9.250000
|
||||
mid_rate: 9.175000
|
||||
effective_date: '2025-01-01'
|
||||
- id: er_cny_usd
|
||||
from_currency: CNY
|
||||
to_currency: USD
|
||||
buy_rate: 0.137900
|
||||
sell_rate: 0.139900
|
||||
mid_rate: 0.138900
|
||||
effective_date: '2025-01-01'
|
||||
|
||||
|
||||
18
json/currency_list.json
Normal file
18
json/currency_list.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"tblname": "currency",
|
||||
"alias": "currency_list",
|
||||
"title": "币种管理",
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"fields": ["id", "name", "symbol", "decimal_places", "is_base", "status"],
|
||||
"alters": {
|
||||
"status": {"uitype": "select", "optiontable": "appcodes_kv", "cond": "parentid='currency_status'"}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/currency_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/currency_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/currency_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
json/exchange_rate_list.json
Normal file
19
json/exchange_rate_list.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"tblname": "exchange_rate",
|
||||
"alias": "exchange_rate_list",
|
||||
"title": "汇率管理",
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"fields": ["from_currency", "to_currency", "buy_rate", "sell_rate", "mid_rate", "effective_date"],
|
||||
"alters": {
|
||||
"from_currency": {"uitype": "select", "optiontable": "currency"},
|
||||
"to_currency": {"uitype": "select", "optiontable": "currency"}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/exchange_rate_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/exchange_rate_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/exchange_rate_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
58
models/currency.json
Normal file
58
models/currency.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "currency",
|
||||
"title": "币种",
|
||||
"primary": ["id"]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "币种代码",
|
||||
"type": "str",
|
||||
"length": 3
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "币种名称",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "symbol",
|
||||
"title": "货币符号",
|
||||
"type": "str",
|
||||
"length": 8
|
||||
},
|
||||
{
|
||||
"name": "decimal_places",
|
||||
"title": "小数位数",
|
||||
"type": "short",
|
||||
"default": 2
|
||||
},
|
||||
{
|
||||
"name": "is_base",
|
||||
"title": "是否本位币",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 16,
|
||||
"default": "active"
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='currency_status'"
|
||||
}
|
||||
]
|
||||
}
|
||||
81
models/exchange_rate.json
Normal file
81
models/exchange_rate.json
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "exchange_rate",
|
||||
"title": "汇率表",
|
||||
"primary": ["id"]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "id",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "from_currency",
|
||||
"title": "源币种",
|
||||
"type": "str",
|
||||
"length": 3
|
||||
},
|
||||
{
|
||||
"name": "to_currency",
|
||||
"title": "目标币种",
|
||||
"type": "str",
|
||||
"length": 3
|
||||
},
|
||||
{
|
||||
"name": "buy_rate",
|
||||
"title": "买入价",
|
||||
"type": "float",
|
||||
"length": 14,
|
||||
"dec": 6
|
||||
},
|
||||
{
|
||||
"name": "sell_rate",
|
||||
"title": "卖出价",
|
||||
"type": "float",
|
||||
"length": 14,
|
||||
"dec": 6
|
||||
},
|
||||
{
|
||||
"name": "mid_rate",
|
||||
"title": "中间价",
|
||||
"type": "float",
|
||||
"length": 14,
|
||||
"dec": 6
|
||||
},
|
||||
{
|
||||
"name": "effective_date",
|
||||
"title": "生效日期",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_currency_pair_date",
|
||||
"idxtype": "unique",
|
||||
"idxfields": ["from_currency", "to_currency", "effective_date"]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "from_currency",
|
||||
"table": "currency",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
},
|
||||
{
|
||||
"field": "to_currency",
|
||||
"table": "currency",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
}
|
||||
]
|
||||
}
|
||||
91
scripts/multi_currency_migration.sql
Normal file
91
scripts/multi_currency_migration.sql
Normal file
@ -0,0 +1,91 @@
|
||||
-- Multi-currency migration for accounting + pricing + llmage
|
||||
-- Execute against the 'accounting' and 'llmage' databases
|
||||
|
||||
-- ============================================================
|
||||
-- 1. accounting DB: currency + exchange_rate tables
|
||||
-- ============================================================
|
||||
USE accounting;
|
||||
|
||||
-- currency table
|
||||
CREATE TABLE IF NOT EXISTS currency (
|
||||
id VARCHAR(3) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(32) NOT NULL,
|
||||
symbol VARCHAR(8) DEFAULT '',
|
||||
decimal_places SMALLINT DEFAULT 2,
|
||||
is_base VARCHAR(1) DEFAULT '0',
|
||||
status VARCHAR(16) DEFAULT 'active'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT IGNORE INTO currency (id, name, symbol, decimal_places, is_base, status) VALUES
|
||||
('CNY', '人民币', '¥', 2, '1', 'active'),
|
||||
('USD', '美元', '$', 2, '0', 'active'),
|
||||
('JPY', '日元', '¥', 0, '0', 'active'),
|
||||
('GBP', '英镑', '£', 2, '0', 'active');
|
||||
|
||||
-- exchange_rate table
|
||||
CREATE TABLE IF NOT EXISTS exchange_rate (
|
||||
id VARCHAR(32) NOT NULL PRIMARY KEY,
|
||||
from_currency VARCHAR(3) NOT NULL,
|
||||
to_currency VARCHAR(3) NOT NULL,
|
||||
buy_rate DOUBLE(14,6) DEFAULT 0,
|
||||
sell_rate DOUBLE(14,6) DEFAULT 0,
|
||||
mid_rate DOUBLE(14,6) DEFAULT 0,
|
||||
effective_date DATE NOT NULL,
|
||||
updated_at DATETIME DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_currency_pair_date
|
||||
ON exchange_rate (from_currency, to_currency, effective_date);
|
||||
|
||||
INSERT IGNORE INTO exchange_rate (id, from_currency, to_currency, buy_rate, sell_rate, mid_rate, effective_date) VALUES
|
||||
('er_usd_cny', 'USD', 'CNY', 7.150000, 7.250000, 7.200000, '2025-01-01'),
|
||||
('er_jpy_cny', 'JPY', 'CNY', 0.048000, 0.049000, 0.048500, '2025-01-01'),
|
||||
('er_gbp_cny', 'GBP', 'CNY', 9.100000, 9.250000, 9.175000, '2025-01-01'),
|
||||
('er_cny_usd', 'CNY', 'USD', 0.137900, 0.139900, 0.138900, '2025-01-01');
|
||||
|
||||
-- appcodes for currency_status
|
||||
INSERT IGNORE INTO appcodes (id, name, hierarchy_flg) VALUES ('currency_status', '币种状态', '0');
|
||||
INSERT IGNORE INTO appcodes_kv (id, parentid, k, v) VALUES
|
||||
('currency_status_active', 'currency_status', 'active', '启用'),
|
||||
('currency_status_inactive', 'currency_status', 'inactive', '停用');
|
||||
|
||||
-- ============================================================
|
||||
-- 2. account table: add currency field
|
||||
-- ============================================================
|
||||
ALTER TABLE account ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'CNY' AFTER org1id;
|
||||
|
||||
-- Drop old unique index and recreate with currency
|
||||
-- (manual: if unique index exists on accounting_orgid+orgid+subjectid+org1id, recreate)
|
||||
-- ALTER TABLE account DROP INDEX idx1;
|
||||
-- CREATE UNIQUE INDEX idx1 ON account (accounting_orgid, orgid, subjectid, org1id, currency);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. acc_detail / bill / bill_detail / ledger: add currency fields
|
||||
-- ============================================================
|
||||
ALTER TABLE acc_detail ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'CNY' AFTER amount;
|
||||
ALTER TABLE acc_detail ADD COLUMN IF NOT EXISTS exchange_rate DOUBLE(14,6) DEFAULT 1.0 AFTER currency;
|
||||
ALTER TABLE acc_detail ADD COLUMN IF NOT EXISTS base_amount DOUBLE(20,2) DEFAULT 0 AFTER exchange_rate;
|
||||
|
||||
ALTER TABLE bill ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'CNY' AFTER amount;
|
||||
ALTER TABLE bill ADD COLUMN IF NOT EXISTS base_amount DOUBLE(20,2) DEFAULT 0 AFTER currency;
|
||||
|
||||
ALTER TABLE bill_detail ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'CNY' AFTER amount;
|
||||
ALTER TABLE bill_detail ADD COLUMN IF NOT EXISTS exchange_rate DOUBLE(14,6) DEFAULT 1.0 AFTER currency;
|
||||
ALTER TABLE bill_detail ADD COLUMN IF NOT EXISTS base_amount DOUBLE(20,2) DEFAULT 0 AFTER exchange_rate;
|
||||
|
||||
ALTER TABLE ledger ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'CNY' AFTER amount;
|
||||
ALTER TABLE ledger ADD COLUMN IF NOT EXISTS exchange_rate DOUBLE(14,6) DEFAULT 1.0 AFTER currency;
|
||||
ALTER TABLE ledger ADD COLUMN IF NOT EXISTS base_amount DOUBLE(20,2) DEFAULT 0 AFTER exchange_rate;
|
||||
|
||||
-- ============================================================
|
||||
-- 4. pricing DB: pricing_program add currency
|
||||
-- ============================================================
|
||||
ALTER TABLE pricing_program ADD COLUMN IF NOT EXISTS currency VARCHAR(3) DEFAULT 'CNY' AFTER description;
|
||||
|
||||
-- ============================================================
|
||||
-- 5. llmage DB: llmusage add currency fields
|
||||
-- ============================================================
|
||||
ALTER TABLE llmusage ADD COLUMN IF NOT EXISTS amount_currency VARCHAR(3) DEFAULT 'CNY' AFTER amount;
|
||||
ALTER TABLE llmusage ADD COLUMN IF NOT EXISTS amount_base DOUBLE(20,2) DEFAULT 0 AFTER amount_currency;
|
||||
ALTER TABLE llmusage ADD COLUMN IF NOT EXISTS cost_currency VARCHAR(3) DEFAULT 'CNY' AFTER cost;
|
||||
ALTER TABLE llmusage ADD COLUMN IF NOT EXISTS cost_base DOUBLE(20,2) DEFAULT 0 AFTER cost_currency;
|
||||
14
wwwroot/api/currency_create.dspy
Normal file
14
wwwroot/api/currency_create.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
# 币种创建
|
||||
ns = {
|
||||
'id': params_kw.get('id', ''),
|
||||
'name': params_kw.get('name', ''),
|
||||
'symbol': params_kw.get('symbol', ''),
|
||||
'decimal_places': params_kw.get('decimal_places', 2),
|
||||
'is_base': params_kw.get('is_base', '0'),
|
||||
'status': params_kw.get('status', 'active'),
|
||||
}
|
||||
if not ns['id'] or not ns['name']:
|
||||
return json.dumps({'success': False, 'message': 'id and name required'})
|
||||
async with get_sor_context(request._run_ns, 'accounting') as sor:
|
||||
await sor.C('currency', ns)
|
||||
return json.dumps({'success': True, 'message': '币种已创建'})
|
||||
7
wwwroot/api/currency_delete.dspy
Normal file
7
wwwroot/api/currency_delete.dspy
Normal file
@ -0,0 +1,7 @@
|
||||
# 币种删除
|
||||
id = params_kw.get('id', '')
|
||||
if not id:
|
||||
return json.dumps({'success': False, 'message': 'id required'})
|
||||
async with get_sor_context(request._run_ns, 'accounting') as sor:
|
||||
await sor.D('currency', {'id': id})
|
||||
return json.dumps({'success': True, 'message': '币种已删除'})
|
||||
12
wwwroot/api/currency_update.dspy
Normal file
12
wwwroot/api/currency_update.dspy
Normal file
@ -0,0 +1,12 @@
|
||||
# 币种更新
|
||||
ns = {}
|
||||
for k in ('name', 'symbol', 'decimal_places', 'is_base', 'status'):
|
||||
v = params_kw.get(k)
|
||||
if v is not None:
|
||||
ns[k] = v
|
||||
ns['id'] = params_kw.get('id', '')
|
||||
if not ns['id']:
|
||||
return json.dumps({'success': False, 'message': 'id required'})
|
||||
async with get_sor_context(request._run_ns, 'accounting') as sor:
|
||||
await sor.U('currency', ns)
|
||||
return json.dumps({'success': True, 'message': '币种已更新'})
|
||||
16
wwwroot/api/exchange_rate_create.dspy
Normal file
16
wwwroot/api/exchange_rate_create.dspy
Normal file
@ -0,0 +1,16 @@
|
||||
# 汇率创建
|
||||
ns = {
|
||||
'id': getID(),
|
||||
'from_currency': params_kw.get('from_currency', ''),
|
||||
'to_currency': params_kw.get('to_currency', ''),
|
||||
'buy_rate': params_kw.get('buy_rate', 0),
|
||||
'sell_rate': params_kw.get('sell_rate', 0),
|
||||
'mid_rate': params_kw.get('mid_rate', 0),
|
||||
'effective_date': params_kw.get('effective_date', curDateString()),
|
||||
'updated_at': timestampstr(),
|
||||
}
|
||||
if not ns['from_currency'] or not ns['to_currency']:
|
||||
return json.dumps({'success': False, 'message': 'from_currency and to_currency required'})
|
||||
async with get_sor_context(request._run_ns, 'accounting') as sor:
|
||||
await sor.C('exchange_rate', ns)
|
||||
return json.dumps({'success': True, 'message': '汇率已创建'})
|
||||
7
wwwroot/api/exchange_rate_delete.dspy
Normal file
7
wwwroot/api/exchange_rate_delete.dspy
Normal file
@ -0,0 +1,7 @@
|
||||
# 汇率删除
|
||||
id = params_kw.get('id', '')
|
||||
if not id:
|
||||
return json.dumps({'success': False, 'message': 'id required'})
|
||||
async with get_sor_context(request._run_ns, 'accounting') as sor:
|
||||
await sor.D('exchange_rate', {'id': id})
|
||||
return json.dumps({'success': True, 'message': '汇率已删除'})
|
||||
13
wwwroot/api/exchange_rate_update.dspy
Normal file
13
wwwroot/api/exchange_rate_update.dspy
Normal file
@ -0,0 +1,13 @@
|
||||
# 汇率更新
|
||||
ns = {}
|
||||
for k in ('from_currency', 'to_currency', 'buy_rate', 'sell_rate', 'mid_rate', 'effective_date'):
|
||||
v = params_kw.get(k)
|
||||
if v is not None:
|
||||
ns[k] = v
|
||||
ns['id'] = params_kw.get('id', '')
|
||||
ns['updated_at'] = timestampstr()
|
||||
if not ns['id']:
|
||||
return json.dumps({'success': False, 'message': 'id required'})
|
||||
async with get_sor_context(request._run_ns, 'accounting') as sor:
|
||||
await sor.U('exchange_rate', ns)
|
||||
return json.dumps({'success': True, 'message': '汇率已更新'})
|
||||
Loading…
x
Reference in New Issue
Block a user