57 lines
2.5 KiB
Python
57 lines
2.5 KiB
Python
"""Exact, nonnegative amounts in their original currency. No FX conversion."""
|
|
import re
|
|
from decimal import Decimal, InvalidOperation
|
|
|
|
CURRENCIES={
|
|
'CNY':2,'USD':2,'HKD':2,'GBP':2,'EUR':2,'JPY':0,'TWD':2,'SGD':2,
|
|
'AUD':2,'CAD':2,'CHF':2,'NZD':2,'KRW':0,'THB':2,'MYR':2,'AED':2,
|
|
'SAR':2,'IDR':2,'INR':2,'PHP':2,'VND':0,'KWD':3,'BHD':3,
|
|
}
|
|
MAX_AMOUNT=Decimal('999999999')
|
|
|
|
|
|
def normalize_amount(value,currency):
|
|
if currency not in CURRENCIES:
|
|
raise ValueError('请选择支持的币种')
|
|
if not isinstance(value,str) or len(value)>30 or not re.fullmatch(r'[0-9]+(?:\.[0-9]+)?',value):
|
|
raise ValueError('金额须为不小于 0 的数字')
|
|
try:
|
|
amount=Decimal(value)
|
|
if amount>MAX_AMOUNT:
|
|
raise ValueError('金额不能超过 999999999')
|
|
places=CURRENCIES[currency]
|
|
normalized=amount.quantize(Decimal(1).scaleb(-places))
|
|
if normalized!=amount:
|
|
raise ValueError(f'{currency} 金额最多保留 {places} 位小数')
|
|
return format(normalized,f'.{places}f')
|
|
except InvalidOperation:
|
|
raise ValueError('请输入有效金额') from None
|
|
|
|
|
|
def format_money(money):
|
|
if money is None:
|
|
return '未填写'
|
|
amount=normalize_amount(money['amount'],money['currency'])
|
|
return f"{money['currency']} {Decimal(amount):,f}"
|
|
|
|
|
|
def apply_balance(balances,cost,operation):
|
|
if operation not in ('deduct','credit') or not cost:
|
|
raise ValueError('更新余额时须填写本次金额和币种')
|
|
currency=cost['currency']
|
|
amount=Decimal(normalize_amount(cost['amount'],currency))
|
|
current=next((item for item in balances if item['currency']==currency),None)
|
|
if operation=='deduct' and current is None:
|
|
raise ValueError(f'尚未记录 {currency} 余额,请先在卡片资料中添加')
|
|
before={'currency':currency,'amount':normalize_amount(current['amount'] if current else '0',currency)}
|
|
remaining=Decimal(before['amount'])-amount if operation=='deduct' else Decimal(before['amount'])+amount
|
|
if remaining<0:
|
|
raise ValueError(f'{currency} 余额不足,可取消更新余额后单独记录费用')
|
|
after={'currency':currency,'amount':normalize_amount(format(remaining,'f'),currency)}
|
|
updated=[dict(item) for item in balances if item['currency']!=currency]
|
|
if current:
|
|
updated=[dict(after) if item['currency']==currency else dict(item) for item in balances]
|
|
else:
|
|
updated.append(dict(after))
|
|
return updated,before,after
|