Skip to content

Commit e994c4d

Browse files
committed
Further cleanup of decimals calculation
1 parent 18585a3 commit e994c4d

File tree

2 files changed

+200
-107
lines changed

2 files changed

+200
-107
lines changed

slip39/invoice/artifact.py

Lines changed: 81 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,12 @@ def conversions_candidates():
490490
self.conversions = conversions # { ("BTC","ETH"): 14.3914, ... }
491491
self.created = datetime.utcnow().astimezone( timezone.utc )
492492

493+
def decimals( self, currency ):
494+
info = self.currencies_proxy[currency]
495+
if info.symbol in self.currencies_alias:
496+
info = self.currencies_alias[info.symbol]
497+
return info.decimals
498+
493499
def headers( self ):
494500
"""Output the headers to use in tabular formatting of iterator. By default, we'd recommend hiding any starting
495501
with an _ prefix (this is the default)."""
@@ -519,19 +525,19 @@ def __iter__( self ):
519525
"""Iterate of lines, computing totals in each Invoice.currencies
520526
521527
Note that numeric values may be int, float or Fraction, and these are retained through
522-
normal mathematical operations.
528+
normal mathematical operations, at full available precision (even if this is beyond the
529+
"decimals" precision of the underlying cryptocurrency).
530+
531+
The number of decimals desired for each line is provided in "_Decimals". The native
532+
number of decimals must be respected in this line's calculations. For example, if eg. a
533+
0-decimals Token is used, the Net, Tax and Amount are rounded to 0 decimals. All
534+
rounding/display decimals should be performed at the final display of the data.
523535
524-
The number of decimals desired for each line is provided in "_Decimals". The native number
525-
of decimals must be respected in this line's calculations. For example, if eg. a
526-
0-decimals Token is used, the Net, Tax and Amount are rounded to 0 decimals. The sums in
527-
various currencies converted from each line are not rounded; the currency ratio and hence
528-
sum is full precision, and is only rounded at the final use.
536+
The sums in various currencies converted from each line are not rounded; the currency
537+
ratio and hence sum is full precision, and is only rounded at the final use.
529538
530539
"""
531540

532-
# Get all the eg. wBTC / USDC value ratios for the Line-item currency, vs. each Invoice
533-
# currency at once, in case the iterator takes considerable time; we don't want to risk that
534-
# memoization "refresh" the token values while generating the invoice!
535541
tot = {
536542
c: 0. for c in self.currencies
537543
}
@@ -540,17 +546,22 @@ def __iter__( self ):
540546
}
541547

542548
for i,line in enumerate( self.lines ):
543-
line_currency = line.currency or INVOICE_CURRENCY
544549
line_amount,line_taxes,line_taxinf = line.net()
550+
line_net = line_amount - line_taxes
545551
# The line's ERC-20 Token (or a known proxy for the named Cryptocurrency), or a
546-
# TokenInfo for the specified known Cryptocurrency.
547-
line_curr = cryptocurrency_proxy( line_currency, w3_url=self.w3_url, use_provider=self.use_provider )
552+
# TokenInfo for the specified known Cryptocurrency. Always displays the underlying
553+
# native Cryptocurrency symbol (if known), even if a "Proxy" is specified, or used
554+
# for pricing.
555+
line_currency = line.currency or INVOICE_CURRENCY # Eg. "Bitcoin", "WBTC", "BTC"
556+
line_curr = self.currencies_proxy[line_currency]
557+
log.info( f"Line currency {line_currency} has proxy {line_curr.symbol:6}: decimals: {line_curr.decimals}" )
558+
if line_curr.symbol in self.currencies_alias:
559+
line_curr = self.currencies_alias[line_curr.symbol]
560+
log.info( f"Line currency {line_currency} alias for {line_curr.symbol:6}: decimals: {line_curr.decimals}" )
548561
line_symbol = line_curr.symbol
549-
line_decimals = line_curr.decimals // 3 if line.decimals is None else line.decimals
550-
551-
line_amount = round( line_amount, line_decimals )
552-
line_taxes = round( line_taxes, line_decimals )
553-
line_net = line_amount - line_taxes
562+
line_decimals = line.decimals
563+
if line_decimals is None:
564+
line_decimals = line_curr.decimals // 3
554565

555566
for c in self.currencies:
556567
if line_curr.symbol == c:
@@ -689,9 +700,7 @@ def can( c ):
689700
#
690701
# TODO: There should be a more sensible / less brittle way to do this.
691702
def deci( c ):
692-
if c in self.currencies_alias:
693-
return self.currencies_alias[c].decimals // 3
694-
return self.currencies_proxy[c].decimals // 3
703+
return self.decimals( c ) // 3
695704

696705
def toti( c ):
697706
return headers_can.index( can( f'_Total {c}' ))
@@ -750,8 +759,18 @@ def taxi( c ):
750759
)
751760

752761
def fmt( val, hdr, coin ):
753-
if can( hdr ) in ( 'price', 'taxes', 'amount' ):
762+
hdr_can = can( hdr )
763+
if hdr_can in ( 'price', 'taxes', 'amount', 'net' ):
764+
# It's a Price, or a computed Taxes/Amount/Net; use the line's cryptocurrency to round
754765
return round( val, deci( coin ))
766+
try:
767+
t,t_coin = hdr.split()
768+
except Exception:
769+
pass
770+
else:
771+
if can( t ) in ('total', 'taxes'):
772+
# It's a 'Total/Taxes COIN' column; use the specified coin's decimals for rounding
773+
return round( val, deci( t_coin ))
755774
return val
756775

757776
# Produce the page line-items. We must round each line-item's numeric price values
@@ -1252,47 +1271,54 @@ def write_invoices(
12521271
invoices: Sequence[Tuple[Invoice,InvoiceMetadata]], # sequence of [ (Invoice, InvoiceMetadata), ... ] or { "<name>": Invoice, ... }
12531272
filename = True, # A file name/Path, if PDF output to file is desired; ''/True implies default., False no file
12541273
**kwds
1255-
):
1274+
) -> Union[InvoiceOutput, Exception]:
12561275
"""Generate unique cryptocurrency account(s) for each client invoice, generate the invoice,
12571276
and (optionally) write them to files. Yields a sequence of the generated invoice PDF names
12581277
and details.
12591278
1279+
If an Exception is raised during Invoice generation, a (None, <Exception>) is generated,
1280+
instead of a ("name", <InvoiceOutput).
1281+
12601282
"""
12611283
if filename is None:
12621284
filename = True
12631285
for invoice,metadata in invoices:
1264-
# Provides the supplied invoice,metadata, and receives the transformed (specialized) metadata.
1265-
_,pdf,metadata = produce_invoice(
1266-
invoice = invoice,
1267-
metadata = metadata,
1268-
**kwds
1269-
)
1286+
try:
1287+
# Provides the supplied invoice,metadata, and receives the transformed (specialized) metadata.
1288+
_,pdf,metadata = produce_invoice(
1289+
invoice = invoice,
1290+
metadata = metadata,
1291+
**kwds
1292+
)
12701293

1271-
accounts = invoice.accounts
1272-
assert accounts, \
1273-
"At least one Cryptocurrency account must be specified"
1294+
accounts = invoice.accounts
1295+
assert accounts, \
1296+
"At least one Cryptocurrency account must be specified"
12741297

1275-
pdf_name = (( '' if filename is True else filename ) or FILENAME_FORMAT ).format(
1276-
name = metadata.number,
1277-
date = datetime.strftime( metadata.date, '%Y-%m-%d' ),
1278-
time = datetime.strftime( metadata.date, '%H.%M.%S'),
1279-
crypto = accounts[0].crypto,
1280-
address = accounts[0].address,
1281-
)
1282-
if not pdf_name.lower().endswith( '.pdf' ):
1283-
pdf_name += '.pdf'
1284-
log.warning( f"Invoice {metadata.number}: {pdf_name}" )
1285-
1286-
path = None
1287-
if filename is not False:
1288-
path = metadata.directory.resolve() / pdf_name
1289-
log.warning( f"Writing Invoice {metadata.number!r} to: {path}" )
1290-
pdf.output( path )
1291-
1292-
invoice_output = InvoiceOutput(
1293-
invoice = invoice,
1294-
metadata = metadata,
1295-
pdf = pdf,
1296-
path = path,
1297-
)
1298-
yield pdf_name, invoice_output
1298+
name = (( '' if filename is True else filename ) or FILENAME_FORMAT ).format(
1299+
name = metadata.number,
1300+
date = datetime.strftime( metadata.date, '%Y-%m-%d' ),
1301+
time = datetime.strftime( metadata.date, '%H.%M.%S'),
1302+
crypto = accounts[0].crypto,
1303+
address = accounts[0].address,
1304+
)
1305+
if not name.lower().endswith( '.pdf' ):
1306+
name += '.pdf'
1307+
log.warning( f"Invoice {metadata.number}: {name}" )
1308+
1309+
path = None
1310+
if filename is not False:
1311+
path = metadata.directory.resolve() / name
1312+
log.warning( f"Writing Invoice {metadata.number!r} to: {path}" )
1313+
pdf.output( path )
1314+
1315+
output = InvoiceOutput(
1316+
invoice = invoice,
1317+
metadata = metadata,
1318+
pdf = pdf,
1319+
path = path,
1320+
)
1321+
except Exception as exc:
1322+
name, output = None, exc
1323+
1324+
yield name, output

0 commit comments

Comments
 (0)