Skip to content

Commit e4ad353

Browse files
committed
Further work toward correct decimal handling
1 parent 92e69e8 commit e4ad353

File tree

5 files changed

+236
-124
lines changed

5 files changed

+236
-124
lines changed

slip39/defaults.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@
178178
# current prices for cryptocurrencies -- if you have access to an Ethereum blockchain (either
179179
# locally or via an HTTPS API like Alchemy), then we can securely and reliably get current prices.
180180
# To avoid conflicts, by convention we upper-case symbols, lower-case full names.
181-
INVOICE_ROWS = 60
181+
INVOICE_ROWS = 65
182+
INVOICE_DESCRIPTION_MAX = 56
182183
INVOICE_CURRENCY = "USD"
183184
INVOICE_PROXIES = {
184185
"USD": "USDC",

slip39/invoice/artifact.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434

3535
from ..api import Account
3636
from ..util import commas, is_listlike, is_mapping
37-
from ..defaults import INVOICE_CURRENCY, INVOICE_ROWS, INVOICE_STRFTIME, INVOICE_DUE, MM_IN, FILENAME_FORMAT
37+
from ..defaults import (
38+
INVOICE_CURRENCY, INVOICE_ROWS, INVOICE_STRFTIME, INVOICE_DUE, INVOICE_DESCRIPTION_MAX,
39+
MM_IN, FILENAME_FORMAT,
40+
)
3841
from ..layout import Region, Text, Image, Box, Coordinate, layout_pdf
3942
from .ethereum import tokeninfo, tokenprices, tokenknown
4043

@@ -45,7 +48,6 @@
4548
4649
Receipt -- The Invoice, marked "PAID"
4750
48-
4951
"""
5052
log = logging.getLogger( "customer" )
5153

@@ -109,19 +111,40 @@ def conversions_table( conversions, symbols=None, greater=None ):
109111

110112
# The columns are typed according to the least generic type that *all* rows are convertible
111113
# into. So, if any row is a string, it'll cause the entire column to be formatted as strings.
112-
return tabulate(
113-
[
114-
[ r ] + list(
115-
( '' if r == c or (r,c) not in conversions
116-
else '' if ( greater and conversions.get( (r,c) ) and conversions.get( (c,r) )
117-
and ( conversions[r,c] < ( greater if isinstance( greater, (float,int) ) else conversions[c,r] )))
118-
else conversions[(r,c)] )
119-
for c in symbols
114+
def fmt( v, d ):
115+
if v:
116+
return round( v, d )
117+
return v
118+
119+
convers_raw = [
120+
[ r ] + list(
121+
(
122+
'' if r == c or (r,c) not in conversions
123+
else '' if ( greater and conversions.get( (r,c) ) and conversions.get( (c,r) )
124+
and ( conversions[r,c] < ( greater if isinstance( greater, (float,int) ) else conversions[c,r] )))
125+
else fmt( conversions[r,c], 8 ) # ie. to the Sat (1/10^8 Bitcoin)
120126
)
121-
for r in symbols
122-
],
123-
headers = [ 'Coin' ] + [ f"in {s}" for s in symbols ],
124-
floatfmt = ',.7g',
127+
for c in symbols
128+
)
129+
for r in symbols
130+
]
131+
# Now, remove any column that are completely blank ('', not None). This will take place for
132+
# worthless (or very low valued) currencies, esp. w/ a small 'greater'. Transpose the
133+
# conversions (so each column is a row), and elide any w/ empty conversions rows. Finally,
134+
# re-transpose back to columns.
135+
headers_raw = [ 'Coin' ] + [ f"in {s}" for s in symbols ]
136+
convers_txp = list( zip( *convers_raw ))
137+
headers_use,convers_use_txp = zip( *[
138+
(hdr,col)
139+
for hdr,col in zip( headers_raw, convers_txp )
140+
if any( c != '' for c in col )
141+
])
142+
convers_use = list( zip( *convers_use_txp ))
143+
144+
return tabulate(
145+
convers_use,
146+
headers = headers_use,
147+
floatfmt = ',.15g',
125148
intfmt = ',',
126149
missingval = '?',
127150
tablefmt = 'orgtbl',
@@ -406,6 +429,7 @@ def __init__(
406429
self.currencies_proxy = currencies_proxy # { "BTC": TokenInfo( "WBTC", ... ), ... }
407430
self.currencies_alias = currencies_alias # { "WBTC": TokenInfo( "BTC", ... ), ... }
408431
self.conversions = conversions # { ("BTC","ETH"): 14.3914, ... }
432+
self.created = datetime.utcnow().astimezone( timezone.utc )
409433

410434
def headers( self ):
411435
"""Output the headers to use in tabular formatting of iterator. By default, we'd recommend hiding any starting
@@ -615,7 +639,7 @@ def taxi( c ):
615639
# And the per-currency Sub-totals (for each page)
616640
[
617641
[
618-
self.currencies_account[c].address,
642+
str( self.currencies_account[c] ),
619643
round( page[-1][taxi( c )], deci( c )) if first else round( page[-1][taxi( c )] - page_prev[-1][taxi( c )], deci( c )),
620644
round( page[-1][toti( c )], deci( c )) if first else round( page[-1][toti( c )] - page_prev[-1][toti( c )], deci( c )),
621645
c,
@@ -631,14 +655,14 @@ def taxi( c ):
631655
'Currency',
632656
),
633657
intfmt = ',',
634-
floatfmt = ',g',
658+
floatfmt = ',.15g',
635659
tablefmt = totalfmt or 'orgtbl',
636660
)
637661

638662
# And the per-currency Totals (up to current page)
639663
total_rows = [
640664
[
641-
self.currencies_account[c].address,
665+
str( self.currencies_account[c] ),
642666
round( page[-1][taxi( c )], deci( c )),
643667
round( page[-1][toti( c )], deci( c )),
644668
c,
@@ -657,13 +681,13 @@ def taxi( c ):
657681
total_rows,
658682
headers = total_headers,
659683
intfmt = ',',
660-
floatfmt = ',g',
684+
floatfmt = ',.15g',
661685
tablefmt = totalfmt or 'orgtbl',
662686
)
663687

664688
def fmt( val, hdr, coin ):
665689
if can( hdr ) in ( 'price', 'taxes', 'amount' ):
666-
return round( val, deci( coin )) # f"{float( val ):,.{deci( coin )}f}" # rounds to designated decimal places, inserts comma
690+
return round( val, deci( coin ))
667691
return val
668692

669693
# Produce the page line-items. We must round each line-item's numeric price values
@@ -706,7 +730,7 @@ def fmt( val, hdr, coin ):
706730
# Presently, the Description column is the only one likely to have a width issue...
707731
maxcolwidths = None
708732
if description_max is None:
709-
description_max = 48
733+
description_max = INVOICE_DESCRIPTION_MAX
710734
if description_max:
711735
desc_i = headers_can.index( can( "Description" ))
712736
maxcolwidths = [
@@ -718,8 +742,8 @@ def fmt( val, hdr, coin ):
718742
table = tabulate(
719743
table_rows,
720744
headers = table_headers,
721-
intfmt = ',', # intfmt,
722-
floatfmt = ',g', # floatfmt,
745+
intfmt = ',',
746+
floatfmt = ',.15g',
723747
tablefmt = tablefmt or 'orgtbl',
724748
maxcolwidths = maxcolwidths,
725749
)
@@ -1009,7 +1033,6 @@ def produce_invoice(
10091033
inv_date: Optional[datetime] = None,
10101034
inv_due: Optional[Any] = None, # another datetime, or args/kwds for datetime_advance
10111035
terms: Optional[str] = None, # eg. "Payable on receipt in $USDC, $ETH, $BTC"
1012-
conversions: Optional[Dict] = None,
10131036
rows: Optional[int] = None,
10141037
paper_format: Any = None, # 'Letter', 'Legal', 'A4', (x,y) dimensions in mm.
10151038
orientation: Optional[str] = None, # available orientations; default portrait, landscape
@@ -1042,7 +1065,7 @@ def produce_invoice(
10421065
# Any datetime WITHOUT a timezone designation is re-interpreted as the local timezone of the
10431066
# invoice issuer, or UTC.
10441067
if inv_date is None:
1045-
inv_date = datetime.utcnow().astimezone( timezone.utc )
1068+
inv_date = invoice.created
10461069
if inv_date.tzname() is None:
10471070
try:
10481071
inv_zone = get_localzone()
@@ -1054,6 +1077,7 @@ def produce_invoice(
10541077
if not isinstance( inv_due, datetime ):
10551078
if not inv_due:
10561079
inv_due = INVOICE_DUE
1080+
log.info( f"Due w/ {inv_due!r}" )
10571081
if is_mapping( inv_due ):
10581082
inv_due = datetime_advance( inv_date, **dict( inv_due ))
10591083
elif is_listlike( inv_due ):
@@ -1081,9 +1105,6 @@ def produce_invoice(
10811105
directory = directory,
10821106
)
10831107

1084-
if conversions is None:
1085-
conversions = {}
1086-
10871108
# Default to full page, given the desired paper and orientation. All PDF dimensions are mm.
10881109
invs_pp,orientation,page_xy,pdf,comp_dim = layout_pdf(
10891110
paper_format = paper_format,
@@ -1130,14 +1151,15 @@ def produce_invoice(
11301151
inv_tpl['inv-client-info-bg'] = layout / '1x1-ffffffbf.png'
11311152

11321153
dets = tabulate( [
1133-
[ 'Invoice #', metadata.number ],
1154+
[ 'Invoice #:', metadata.number ],
11341155
[ 'Date:', metadata.date.strftime( INVOICE_STRFTIME ) ],
1135-
[ 'Due:', metadata.date.strftime( INVOICE_STRFTIME ) ],
1156+
[ 'Due:', metadata.due.strftime( INVOICE_STRFTIME ) ],
11361157
], colalign=( 'right', 'left' ), tablefmt='plain' )
11371158

11381159
exch = conversions_table( invoice.conversions )
1160+
exch_date = invoice.created.strftime( INVOICE_STRFTIME )
11391161

1140-
inv_tpl['inv-table'] = '\n\n'.join( (dets, tbl, f"Conversion ratios used:\n{exch}" ) ) # f"{tbl}\n\n{80 * ( '=' if last else '-' )}\n{tot if last else sub}"
1162+
inv_tpl['inv-table'] = '\n\n'.join( (dets, tbl, f"Conversion ratios used (est. {exch_date}):\n{exch}" ) )
11411163
inv_tpl['inv-table-bg'] = layout / '1x1-ffffffbf.png'
11421164

11431165
inv_tpl.render( offsetx=offsetx, offsety=offsety )

0 commit comments

Comments
 (0)