2929
3030import 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
3537from ..api import Account
3638from ..util import commas , is_listlike , is_mapping
3739from ..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)
4143from ..layout import Region , Text , Image , Box , Coordinate , layout_pdf
4244from .ethereum import tokeninfo , tokenprices , tokenknown
5153"""
5254log = 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