Skip to content

Commit 9fdbaf9

Browse files
committed
Further formatting work, Terms calculation
1 parent e4ad353 commit 9fdbaf9

File tree

4 files changed

+136
-105
lines changed

4 files changed

+136
-105
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
base58 >=2.0.1,<3
22
chacha20poly1305 >=0.0.3
33
click >=8.1.3,<9
4-
crypto-licensing >=3.0.3,<4
4+
crypto-licensing >=3.0.4,<4
55
cx_Freeze >=6.12 ; sys_platform == "win32"
66
fpdf2 >=2.5.7,<3
77
hdwallet >=2.2.1,<3

slip39/defaults.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
BUSINESS_CARD = (2, 3+1/2), 1/32 # noqa: E241
5252
CREDIT_CARD = (2+1/4, 3+3/8), 1/32
5353
INDEX_CARD = (3, 5), 1/16 # noqa: E241
54-
HALF_LETTER = (13.5/3,8), 1/8 # noqa: E241 (actually, 2/letter, 3/legal)
55-
THIRD_LETTER = (13.5/4,8), 1/8 # noqa: E241 (actually, 3/letter, 4/legal)
56-
QUARTER_LETTER = (10.5/4,8), 1/8 # noqa: E241 (actually, 4/letter, 5/legal)
54+
HALF_LETTER = (13.5/3,8), 1/8 # noqa: E241 (actually 2/letter, 3/legal)
55+
THIRD_LETTER = (13.5/4,8), 1/8 # noqa: E241 (actually 3/letter, 4/legal)
56+
QUARTER_LETTER = (10.5/4,8), 1/8 # noqa: E241 (actually 4/letter, 5/legal)
5757
PHOTO_CARD = (3+1/2, 5+1/2), 1/16 # prints on 4x6 photo paper w/ 1/4" default outer border
5858

5959
# SLIP-39 Mnemonic Card Sizes
@@ -114,7 +114,8 @@
114114
59: (12, 5), # 512-bit seed, eg. from BIP-39 (Unsupported on Trezor)
115115
}
116116

117-
# Separators for groups of Mnemonics, and those that indicate the continuation/last line of a Mnemonic phrase
117+
# Separators for groups of Mnemonics, and those that indicate the continuation/last line of a
118+
# Mnemonic phrase
118119
MNEM_PREFIX = {
119120
20: '{',
120121
33: '╭╰',
@@ -172,13 +173,15 @@
172173
SMTP_TO = "[email protected]"
173174
SMTP_FROM = "[email protected]"
174175

175-
# Invoice options. Presently, only highly-liquid ERC-20 tokens present in Ethereum AMM (Automatic
176-
# Market Maker) systems should be used in invoices, since we use 1Inch's "Off-Chain Oracle" smart
177-
# contract to get current market values. This prevents us from needing to "trust" anyone to obtain
178-
# current prices for cryptocurrencies -- if you have access to an Ethereum blockchain (either
179-
# locally or via an HTTPS API like Alchemy), then we can securely and reliably get current prices.
180-
# To avoid conflicts, by convention we upper-case symbols, lower-case full names.
181-
INVOICE_ROWS = 65
176+
# Invoice options. Presently, only highly-liquid ERC-20 tokens present in Ethereum AMM
177+
# (Automatic Market Maker) systems should be used in invoices, since we use 1Inch's "Off-Chain
178+
# Oracle" smart contract to get current market values. This prevents us from needing to "trust"
179+
# anyone to obtain current prices for cryptocurrencies -- if you have access to an Ethereum
180+
# blockchain (either locally or via an HTTPS API like Alchemy), then we can securely and reliably
181+
# get current prices. To avoid conflicts, by convention we upper-case symbols, lower-case full
182+
# names.
183+
INVOICE_FORMAT = 'totalize' # 'presto' # 'orgtbl'
184+
INVOICE_ROWS = 60
182185
INVOICE_DESCRIPTION_MAX = 56
183186
INVOICE_CURRENCY = "USD"
184187
INVOICE_PROXIES = {
@@ -191,5 +194,5 @@
191194
}
192195

193196
# Invoice times; very explicit about timezones, b/c short zone names are non-deterministic
194-
INVOICE_DUE = dict( months=1 ) # Default terms are Net 1 month (~30 days)
195-
INVOICE_STRFTIME = "%c %z %Z" # Eg. "Wed Aug 16 21:30:00 1988 +0000 UTC"
197+
INVOICE_DUE = dict( months=1 ) # Default terms: Net 1 month (~30 days)
198+
INVOICE_STRFTIME = "%c %z %Z" # "Wed Aug 16 21:30:00 1988 +0000 UTC"

slip39/invoice/artifact.py

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@
2929

3030
import fpdf
3131

32-
from tabulate import tabulate, SEPARATING_LINE
33-
from crypto_licensing.misc import get_localzone
32+
from tabulate import (
33+
tabulate, SEPARATING_LINE, multiline_formats, _table_formats, TableFormat, Line, DataRow
34+
)
35+
from crypto_licensing.misc import get_localzone, Duration
3436

3537
from ..api import Account
3638
from ..util import commas, is_listlike, is_mapping
3739
from ..defaults import (
3840
INVOICE_CURRENCY, INVOICE_ROWS, INVOICE_STRFTIME, INVOICE_DUE, INVOICE_DESCRIPTION_MAX,
39-
MM_IN, FILENAME_FORMAT,
41+
INVOICE_FORMAT, MM_IN, FILENAME_FORMAT,
4042
)
4143
from ..layout import Region, Text, Image, Box, Coordinate, layout_pdf
4244
from .ethereum import tokeninfo, tokenprices, tokenknown
@@ -51,6 +53,18 @@
5153
"""
5254
log = logging.getLogger( "customer" )
5355

56+
# Custom tabulate format that provides "====" SEPARATING_LINE between line-items and totals
57+
_table_formats["totalize"] = TableFormat(
58+
lineabove = Line("", "-", " ", ""),
59+
linebelowheader = Line("", "-", " ", ""),
60+
linebetweenrows = Line("", "=", " ", ""),
61+
linebelow = Line("", "-", " ", ""),
62+
headerrow = DataRow("", " ", ""),
63+
datarow = DataRow("", " ", ""),
64+
padding = 0,
65+
with_header_hide = ["lineabove", "linebelow", "linebetweenrows"],
66+
)
67+
multiline_formats["totalize"] = "totalize"
5468

5569
# An invoice line-Item contains the details of a component of a transaction. It is priced in a
5670
# currency, with a number of units 'qty', and a 'price' per unit.
@@ -92,16 +106,16 @@ def net( self ):
92106
taxinf = 'no tax'
93107
if self.tax:
94108
if self.tax < 1:
95-
taxinf = f"{round( float( self.tax * 100 ), 2):g}% added"
109+
taxinf = f"{round( float( self.tax * 100 ), 2):g}% add"
96110
taxes = amount * self.tax
97111
amount += taxes
98112
elif self.tax > 1:
99-
taxinf = f"{round( float(( self.tax - 1 ) * 100 ), 2):g}% incl."
113+
taxinf = f"{round( float(( self.tax - 1 ) * 100 ), 2):g}% inc"
100114
taxes = amount - amount / self.tax
101115
return amount, taxes, taxinf # denominated in self.currencies
102116

103117

104-
def conversions_table( conversions, symbols=None, greater=None ):
118+
def conversions_table( conversions, symbols=None, greater=None, tablefmt=None ):
105119
if symbols is None:
106120
symbols = sorted( set( sum( conversions.keys(), () )))
107121
if greater is None:
@@ -147,7 +161,7 @@ def fmt( v, d ):
147161
floatfmt = ',.15g',
148162
intfmt = ',',
149163
missingval = '?',
150-
tablefmt = 'orgtbl',
164+
tablefmt = tablefmt or INVOICE_FORMAT,
151165
)
152166

153167

@@ -392,8 +406,9 @@ def __init__(
392406

393407
# Resolve all resolvable conversions (ie. from any supplied), see what's left
394408
while ( remaining := conversions_remaining( conversions ) ) and not isinstance( remaining, str ):
395-
print( f"Working: \n{conversions_table( conversions )}" )
396-
log.warning( f"{'Remaining' if remaining else 'Resolved'}:\n{conversions_table( conversions )}\n{f'==> {remaining}' if remaining else ''}" )
409+
if log.isEnabledFor( logging.DEBUG ):
410+
log.debug( f"Working: \n{conversions_table( conversions )}" )
411+
log.info( f"{'Remaining' if remaining else 'Resolved'}:\n{conversions_table( conversions )}\n{f'==> {remaining}' if remaining else ''}" )
397412

398413
while remaining:
399414
# There are unresolved LineItem -> Invoice currencies. We need to get a price ratio between
@@ -421,8 +436,9 @@ def __init__(
421436
else:
422437
raise RuntimeError( f"Failed to resolve {remaining}, using candidates {commas( candidates, final='and' )}" )
423438
while ( remaining := conversions_remaining( conversions ) ) and not isinstance( remaining, str ):
424-
print( f"Working: \n{conversions_table( conversions )}" )
425-
log.warning( f"{'Remaining' if remaining else 'Resolved'}:\n{conversions_table( conversions )}\n{f'==> {remaining}' if remaining else ''}" )
439+
if log.isEnabledFor( logging.DEBUG ):
440+
log.debug( f"Working: \n{conversions_table( conversions )}" )
441+
log.info( f"{'Remaining' if remaining else 'Resolved'}:\n{conversions_table( conversions )}\n{f'==> {remaining}' if remaining else ''}" )
426442

427443
self.currencies = currencies # { "USDC", "BTC", ... }
428444
self.currencies_account = currencies_account # { "USDC": "0xaBc...12D", "BTC": "bc1...", ... }
@@ -656,7 +672,7 @@ def taxi( c ):
656672
),
657673
intfmt = ',',
658674
floatfmt = ',.15g',
659-
tablefmt = totalfmt or 'orgtbl',
675+
tablefmt = tablefmt or INVOICE_FORMAT,
660676
)
661677

662678
# And the per-currency Totals (up to current page)
@@ -682,7 +698,7 @@ def taxi( c ):
682698
headers = total_headers,
683699
intfmt = ',',
684700
floatfmt = ',.15g',
685-
tablefmt = totalfmt or 'orgtbl',
701+
tablefmt = tablefmt or INVOICE_FORMAT,
686702
)
687703

688704
def fmt( val, hdr, coin ):
@@ -744,7 +760,7 @@ def fmt( val, hdr, coin ):
744760
headers = table_headers,
745761
intfmt = ',',
746762
floatfmt = ',.15g',
747-
tablefmt = tablefmt or 'orgtbl',
763+
tablefmt = tablefmt or INVOICE_FORMAT,
748764
maxcolwidths = maxcolwidths,
749765
)
750766

@@ -1031,7 +1047,7 @@ def produce_invoice(
10311047
inv_label: Optional[str] = None,
10321048
inv_number: Optional[Union[str,int]] = None, # eg. "CLIENT-20230930-0001" (default: <client>-<date>-<num>)
10331049
inv_date: Optional[datetime] = None,
1034-
inv_due: Optional[Any] = None, # another datetime, or args/kwds for datetime_advance
1050+
inv_due: Optional[Any] = None, # another datetime, timedelta, or args/kwds for datetime_advance
10351051
terms: Optional[str] = None, # eg. "Payable on receipt in $USDC, $ETH, $BTC"
10361052
rows: Optional[int] = None,
10371053
paper_format: Any = None, # 'Letter', 'Legal', 'A4', (x,y) dimensions in mm.
@@ -1041,8 +1057,9 @@ def produce_invoice(
10411057
):
10421058
"""Produces a PDF containing the supplied Invoice details, optionally with a PAID watermark.
10431059
1044-
10451060
"""
1061+
if inv_label is None:
1062+
inv_label = 'Invoice'
10461063
if isinstance( directory, (str,type(None))):
10471064
directory = Path( directory or '.' )
10481065
assert isinstance( directory, (Path,type(None)))
@@ -1087,6 +1104,10 @@ def produce_invoice(
10871104
else:
10881105
raise ValueError( f"Unsupported Invoice due date: {inv_due!r}" )
10891106
log.info( f"Due: {inv_due.strftime( INVOICE_STRFTIME )}" )
1107+
if terms is None:
1108+
terms = f"Net {Duration( inv_due - inv_date )!r}"
1109+
log.info( f"Terms: {terms}" )
1110+
10901111
if inv_number is None:
10911112
cli = client.name.split()[0].upper()[:3] if client else 'INV'
10921113
ymd = inv_date.strftime( '%Y%m%d' )
@@ -1137,7 +1158,7 @@ def produce_invoice(
11371158
#last = i + 1 == len( details )
11381159
inv_tpl['inv-image'] = inv_image
11391160
inv_tpl['inv-logo'] = inv_logo
1140-
inv_tpl['inv-label'] = f"{'Invoice' if inv_label is None else inv_label} (page {p+1}/{len( details )})"
1161+
inv_tpl['inv-label'] = f"{inv_label} (page {p+1}/{len( details )})"
11411162
inv_tpl['inv-label-bg'] = layout / '1x1-ffffffbf.png'
11421163

11431164
inv_tpl['inv-vendor'] = vendor.name
@@ -1150,16 +1171,20 @@ def produce_invoice(
11501171
inv_tpl['inv-client-info'] = '\n'.join( client.info )
11511172
inv_tpl['inv-client-info-bg'] = layout / '1x1-ffffffbf.png'
11521173

1153-
dets = tabulate( [
1154-
[ 'Invoice #:', metadata.number ],
1155-
[ 'Date:', metadata.date.strftime( INVOICE_STRFTIME ) ],
1156-
[ 'Due:', metadata.due.strftime( INVOICE_STRFTIME ) ],
1157-
], colalign=( 'right', 'left' ), tablefmt='plain' )
1174+
dets = tabulate(
1175+
[
1176+
[ f'{inv_label} #:', metadata.number ],
1177+
[ 'Date:', metadata.date.strftime( INVOICE_STRFTIME ) ],
1178+
[ 'Due:', metadata.due.strftime( INVOICE_STRFTIME ) ],
1179+
[ 'Terms:', terms ]
1180+
],
1181+
colalign=( 'right', 'left' ), tablefmt='plain'
1182+
)
11581183

1159-
exch = conversions_table( invoice.conversions )
1184+
exch = conversions_table( invoice.conversions, greater=1 )
11601185
exch_date = invoice.created.strftime( INVOICE_STRFTIME )
11611186

1162-
inv_tpl['inv-table'] = '\n\n'.join( (dets, tbl, f"Conversion ratios used (est. {exch_date}):\n{exch}" ) )
1187+
inv_tpl['inv-table'] = '\n\n'.join( (dets, tbl, f"CONVERSION RATIOS (est. {exch_date}):\n{exch}" ) )
11631188
inv_tpl['inv-table-bg'] = layout / '1x1-ffffffbf.png'
11641189

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

0 commit comments

Comments
 (0)