Skip to content

Commit 92e69e8

Browse files
committed
Include conversion ratios used on invoice
1 parent c7a63aa commit 92e69e8

File tree

2 files changed

+109
-43
lines changed

2 files changed

+109
-43
lines changed

slip39/invoice/artifact.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,23 +99,29 @@ def net( self ):
9999
return amount, taxes, taxinf # denominated in self.currencies
100100

101101

102-
def conversions_table( conversions, symbols=None ):
102+
def conversions_table( conversions, symbols=None, greater=None ):
103103
if symbols is None:
104104
symbols = sorted( set( sum( conversions.keys(), () )))
105+
if greater is None:
106+
greater = .001
107+
105108
symbols = list( symbols )
106109

107110
# The columns are typed according to the least generic type that *all* rows are convertible
108111
# into. So, if any row is a string, it'll cause the entire column to be formatted as strings.
109112
return tabulate(
110113
[
111114
[ r ] + list(
112-
1 if r == c else '' if (r,c) not in conversions else conversions.get( (r,c) )
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)] )
113119
for c in symbols
114120
)
115121
for r in symbols
116122
],
117-
headers = [ 'Coin' ] + list( symbols ),
118-
floatfmt = ',.6g',
123+
headers = [ 'Coin' ] + [ f"in {s}" for s in symbols ],
124+
floatfmt = ',.7g',
119125
intfmt = ',',
120126
missingval = '?',
121127
tablefmt = 'orgtbl',
@@ -524,6 +530,7 @@ def tables(
524530
columns = None, # list (ordered) set/predicate (unordered)
525531
totalfmt = None,
526532
totalize = None, # Final page includes totalization
533+
description_max = None, # Default to some reasonable max
527534
**kwds # page, rows, ...
528535
):
529536
"""Tabulate columns into textual tables. Each page yields 3 items:
@@ -659,7 +666,8 @@ def fmt( val, hdr, coin ):
659666
return round( val, deci( coin )) # f"{float( val ):,.{deci( coin )}f}" # rounds to designated decimal places, inserts comma
660667
return val
661668

662-
# Produce the page line-items. TODO: This may now include per-Coin totals on the final page.
669+
# Produce the page line-items. We must round each line-item's numeric price values
670+
# according to the target coin.
663671
table_rows = [
664672
[
665673
fmt( line[i], h, line[headers_can.index( can( 'Coin' ))] )
@@ -695,12 +703,25 @@ def fmt( val, hdr, coin ):
695703
row.append( None )
696704
table_rows.append( row )
697705

706+
# Presently, the Description column is the only one likely to have a width issue...
707+
maxcolwidths = None
708+
if description_max is None:
709+
description_max = 48
710+
if description_max:
711+
desc_i = headers_can.index( can( "Description" ))
712+
maxcolwidths = [
713+
description_max if i == desc_i else None
714+
for i in selected
715+
]
716+
log.info( f"Maximum col widths {commas( ( f'{h}: {w}' for h,w in zip( table_headers, maxcolwidths ) ), final='and' )}" )
717+
698718
table = tabulate(
699719
table_rows,
700720
headers = table_headers,
701721
intfmt = ',', # intfmt,
702722
floatfmt = ',g', # floatfmt,
703723
tablefmt = tablefmt or 'orgtbl',
724+
maxcolwidths = maxcolwidths,
704725
)
705726

706727
yield page, table, subtotal, total
@@ -1108,13 +1129,15 @@ def produce_invoice(
11081129
inv_tpl['inv-client-info'] = '\n'.join( client.info )
11091130
inv_tpl['inv-client-info-bg'] = layout / '1x1-ffffffbf.png'
11101131

1111-
det = tabulate( [
1132+
dets = tabulate( [
11121133
[ 'Invoice #', metadata.number ],
11131134
[ 'Date:', metadata.date.strftime( INVOICE_STRFTIME ) ],
11141135
[ 'Due:', metadata.date.strftime( INVOICE_STRFTIME ) ],
11151136
], colalign=( 'right', 'left' ), tablefmt='plain' )
11161137

1117-
inv_tpl['inv-table'] = '\n\n'.join( (det, tbl ) ) # f"{tbl}\n\n{80 * ( '=' if last else '-' )}\n{tot if last else sub}"
1138+
exch = conversions_table( invoice.conversions )
1139+
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}"
11181141
inv_tpl['inv-table-bg'] = layout / '1x1-ffffffbf.png'
11191142

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

slip39/invoice/artifact_test.py

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ def test_conversions():
3232
c1_tbl = conversions_table( c1 )
3333
print( '\n' + c1_tbl )
3434
assert c1_tbl == """\
35-
| Coin | BTC | ETH | USD |
36-
|--------+-------+-------+-----------|
37-
| BTC | 1 | ? | 23,456.8 |
38-
| ETH | | 1 | 1,234.56 |
39-
| USD | | | 1 |"""
35+
| Coin | in BTC | in ETH | in USD |
36+
|--------+----------+----------+-----------|
37+
| BTC | | ? | 23,456.78 |
38+
| ETH | | | 1,234.56 |
39+
| USD | | | |"""
4040

4141
c_simple = dict( c1 )
4242
c_simple_i = 0
@@ -46,11 +46,11 @@ def test_conversions():
4646
c_simple_tbl = conversions_table( c_simple )
4747
print( c_simple_tbl )
4848
assert c_simple_tbl == """\
49-
| Coin | BTC | ETH | USD |
50-
|--------+-------------+--------------+-----------|
51-
| BTC | 1 | 19.0001 | 23,456.8 |
52-
| ETH | 0.0526313 | 1 | 1,234.56 |
53-
| USD | 4.26316e-05 | 0.000810005 | 1 |"""
49+
| Coin | in BTC | in ETH | in USD |
50+
|--------+-------------+-----------+-----------|
51+
| BTC | | 19.000113 | 23,456.78 |
52+
| ETH | 0.052631265 | | 1,234.56 |
53+
| USD | | | |"""
5454

5555
c_w_doge = dict( c1, ) | { ('DOGE','BTC'): .00000385, ('DOGE','USD'): None }
5656
c_w_doge_i = 0
@@ -74,12 +74,22 @@ def test_conversions():
7474
c_w_doge_tbl = conversions_table( c_w_doge )
7575
print( c_w_doge_tbl )
7676
assert c_w_doge_tbl == """\
77-
| Coin | BTC | DOGE | ETH | USD |
78-
|--------+-------------+--------------+--------------+----------------|
79-
| BTC | 1 | 259,740 | 19.0001 | 23,456.8 |
80-
| DOGE | 3.85e-06 | 1 | 7.31504e-05 | 0.0903086 |
81-
| ETH | 0.0526313 | 13,670.5 | 1 | 1,234.56 |
82-
| USD | 4.26316e-05 | 11.0731 | 0.000810005 | 1 |"""
77+
| Coin | in BTC | in DOGE | in ETH | in USD |
78+
|--------+----------+----------------+-----------+-----------|
79+
| BTC | | 259,740.26 | 19.000113 | 23,456.78 |
80+
| DOGE | | | | |
81+
| ETH | | 13,670.458 | | 1,234.56 |
82+
| USD | | 11.073142 | | |"""
83+
84+
c_w_doge_all = conversions_table( c_w_doge, greater=False )
85+
print( c_w_doge_all )
86+
assert c_w_doge_all == """\
87+
| Coin | in BTC | in DOGE | in ETH | in USD |
88+
|--------+---------------+----------------+----------------+------------------|
89+
| BTC | | 259,740.26 | 19.000113 | 23,456.78 |
90+
| DOGE | 3.85e-06 | | 7.3150437e-05 | 0.090308603 |
91+
| ETH | 0.052631265 | 13,670.458 | | 1,234.56 |
92+
| USD | 4.2631597e-05 | 11.073142 | 0.00081000518 | |"""
8393

8494
c_bad = dict( c1, ) | { ('DOGE','USD'): None }
8595
with pytest.raises( Exception ) as c_bad_exc:
@@ -107,7 +117,7 @@ def test_conversions():
107117
LineItem(
108118
description = "More Widgets",
109119
units = 2500,
110-
price = Fraction( 201, 1000 ),
120+
price = Fraction( 201, 100000 ),
111121
tax = Fraction( 5, 100 ), # exclusive
112122
currency = 'ETH',
113123
),
@@ -116,7 +126,7 @@ def test_conversions():
116126
LineItem(
117127
description = "Something else, very detailed and elaborate to explain",
118128
units = 100,
119-
price = Fraction( 201, 10000 ),
129+
price = Fraction( 201, 100000 ),
120130
tax = Fraction( 105, 100 ), # inclusive
121131
decimals = 8,
122132
currency = 'Bitcoin',
@@ -126,7 +136,7 @@ def test_conversions():
126136
LineItem(
127137
description = "Buy some Holo hosting",
128138
units = 12,
129-
price = 10000,
139+
price = 12345.6,
130140
tax = Fraction( 5, 100 ), # inclusive
131141
currency = 'HoloToken',
132142
),
@@ -294,41 +304,74 @@ def test_tabulate( tmp_path ):
294304

295305
inv_date = parse_datetime( "2021-01-01 00:00:00.1 Canada/Pacific" )
296306

297-
(paper_format,orientation),pdf,metadata = produce_invoice(
298-
invoice = shorter_invoice,
299-
inv_date = inv_date,
300-
vendor = Contact(
301-
name = "Dominion Research & Development Corp.",
302-
contact = "Perry Kundert <[email protected]>",
303-
phone = "+1-780-970-8148",
304-
address = """\
307+
vendor = Contact(
308+
name = "Dominion Research & Development Corp.",
309+
contact = "Perry Kundert <[email protected]>",
310+
phone = "+1-780-970-8148",
311+
address = """\
305312
275040 HWY 604
306313
Lacombe, AB T4L 2N3
307314
CANADA
308315
""",
309-
billing = """\
316+
billing = """\
310317
RR#3, Site 1, Box 13
311318
Lacombe, AB T4L 2N3
312319
CANADA
313320
""",
314-
),
315-
client = Contact(
316-
name = "Awesome, Inc.",
317-
contact = "Great Guy <[email protected]>",
318-
address = """\
321+
)
322+
client = Contact(
323+
name = "Awesome, Inc.",
324+
contact = "Great Guy <[email protected]>",
325+
address = """\
319326
123 Awesome Ave.
320327
Schenectady, NY 12345
321328
USA
322329
""",
323-
),
330+
)
331+
332+
(paper_format,orientation),pdf,metadata = produce_invoice(
333+
invoice = shorter_invoice,
334+
inv_date = inv_date,
335+
vendor = vendor,
336+
client = client,
324337
directory = test,
325-
inv_image = 'dominionrnd-invoice.png', # Full page background image
338+
#inv_image = 'dominionrnd-invoice.png', # Full page background image
326339
inv_logo = 'dominionrnd-logo.png', # Logo 16/9 in bottom right of header
327340
inv_label = 'Quote',
328341
)
329342

330343
print( f"Invoice metadata: {metadata}" )
331344
temp = Path( tmp_path )
332-
path = temp / 'invoice.pdf'
345+
path = temp / 'invoice-shorter.pdf'
346+
pdf.output( path )
347+
print( f"Invoice saved: {path}" )
348+
349+
# Finally, generate invoice with all rows, and all conversions from blockchain Oracle (except
350+
# XRP, for which we do not have an oracle, so must provide an estimate from another source...)
351+
complete_invoice = Invoice(
352+
[
353+
line
354+
for line,_ in line_amounts
355+
],
356+
currencies = ["HOT", "ETH", "BTC", "USD"],
357+
accounts = accounts,
358+
conversions = {
359+
("BTC","XRP"): 60000,
360+
}
361+
)
362+
363+
(paper_format,orientation),pdf,metadata = produce_invoice(
364+
invoice = complete_invoice,
365+
inv_date = inv_date,
366+
vendor = vendor,
367+
client = client,
368+
directory = test,
369+
#inv_image = 'dominionrnd-invoice.png', # Full page background image
370+
inv_logo = 'dominionrnd-logo.png', # Logo 16/9 in bottom right of header
371+
)
372+
373+
print( f"Invoice metadata: {metadata}" )
374+
temp = Path( tmp_path )
375+
path = temp / 'invoice-complete.pdf'
333376
pdf.output( path )
334377
print( f"Invoice saved: {path}" )

0 commit comments

Comments
 (0)