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.providerid = od['providerid']
|
||||||
self.summary = f'{self.action}|{self.customerid}|{self.resellerid}|{self.productid}'
|
self.summary = f'{self.action}|{self.customerid}|{self.resellerid}|{self.productid}'
|
||||||
self.variable = od.variable
|
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):
|
async def get_orgid_by_trans_role(self, sor, leg, role):
|
||||||
if role == 'owner':
|
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 .recharge import RechargeBiz, recharge_accounting
|
||||||
from .consume import consume_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 .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):
|
async def all_my_accounts(request):
|
||||||
env = request._run_ns
|
env = request._run_ns
|
||||||
@ -83,6 +84,11 @@ def load_accounting():
|
|||||||
g.get_credit_stats_web = get_credit_stats_web
|
g.get_credit_stats_web = get_credit_stats_web
|
||||||
g.get_my_credits_web = get_my_credits_web
|
g.get_my_credits_web = get_my_credits_web
|
||||||
g.get_all_credits_web = get_all_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):
|
async def get_credit_stats_web(request):
|
||||||
|
|||||||
@ -57,4 +57,68 @@ appcodes_kv:
|
|||||||
parentid: credit_status
|
parentid: credit_status
|
||||||
k: expired
|
k: expired
|
||||||
v: 已过期
|
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