@@ -207,24 +207,33 @@ def cryptocurrency_symbol( name, chain=None, w3_url=None, use_provider=None ):
207207
208208
209209def cryptocurrency_proxy ( name , decimals = None , chain = None , w3_url = None , use_provider = None ):
210- """Return the named ERC-20 Token (or a known "Proxy" token, eg. BTC -> WBTC). Otherwise, if it is
211- a recognized core supported Cryptocurrency, return a TokenInfo useful for formatting.
210+ """Return the named ERC-20 Token (or a known "Proxy" token, eg. BTC -> WBTC).
212211
213212 """
214213 try :
215214 return tokeninfo ( name , w3_url = w3_url , use_provider = use_provider )
216215 except Exception as exc :
217216 log .info ( f"Could not identify currency { name !r} as an ERC-20 Token: { exc } " )
218217 # Not a known Token; a core known Cryptocurrency?
218+ return cryptocurrency_known ( name , decimals = decimals )
219+
220+
221+ def cryptocurrency_known ( name , decimals = None ):
222+ """If it is a recognized core supported Cryptocurrency, return a TokenInfo useful for formatting.
223+ Since Bitcoin (and other similar cryptocurrencies) are typically assumed to have 8 decimal
224+ places (the Sat(oshi) is 1/10^8 of a Bitcoin), we'll make the default decimals//3 precision work
225+ out to 8).
226+
227+ """
219228 try :
220229 symbol = Account .supported ( name )
221230 except ValueError as exc :
222231 log .info ( f"Failed to identify currency { name !r} as a supported Cryptocurrency: { exc } " )
223232 raise
224233 return TokenInfo (
225- name = name ,
234+ name = Account . CRYPTO_SYMBOLS [ symbol ] ,
226235 symbol = symbol ,
227- decimals = 18 if decimals is None else decimals ,
236+ decimals = 24 if decimals is None else decimals ,
228237 icon = next ( ( Path ( __file__ ).resolve ().parent / "Cryptos" ).glob ( symbol + '*.*' ), None ),
229238 )
230239
@@ -253,7 +262,8 @@ class Invoice:
253262 Can emit the line items in groups with summary sub-totals and a final total.
254263
255264 Reframes the price of each line in terms of the Invoice's currencies (default: USD), providing
256- "Total <SYMBOL>" and "Taxes <SYMBOL>" for each Cryptocurrency symbols.
265+ "Total <SYMBOL>" and "Taxes <SYMBOL>" for each Cryptocurrency symbols. NOTE: These are a
266+ *running* Total; the LineItem Amount is each line's total, in terms of that line's currency.
257267
258268 Now that we have Web3 details, we can query tokeninfos and hence format currency decimals
259269 correctly.
@@ -381,7 +391,7 @@ def __init__(
381391 log .warning ( f"Ignoring { c } : { exc } " )
382392 continue
383393 if conversions .get ( (c ,two .symbol ) ) is None or conversions .get ( (one .symbol ,two .symbol ) ) is None :
384- log .info ( f"Updating { c :>6} /{ two .symbol :<6} = { conversions .get ( (c ,two .symbol ) )} "
394+ log .info ( f"Updated { c :>6} /{ two .symbol :<6} = { conversions .get ( (c ,two .symbol ) )} "
385395 f" and { one .symbol :>6} /{ two .symbol :<6} = { conversions .get ( (one .symbol ,two .symbol ) )} ,"
386396 f" to: { ratio } " )
387397 conversions [c ,two .symbol ] = ratio
@@ -563,8 +573,10 @@ def can( c ):
563573
564574 log .info ( f"Tabulating indices { commas (selected , final = 'and' )} : { commas ( headers_selected , final = 'and' )} " )
565575 pages = list ( self .pages ( * args , ** kwds ))
566- # Overall formatted decimals for entire invoice; use the greatest desired decimals of any
567- # token/line. Individual line items may have been rounded to lower decimals.
576+
577+ # Overall formatted decimals for the entire invoice; use the greatest desired decimals of
578+ # any token/line, based on Cryptocurrency proxies. Individual line items may have been
579+ # rounded to lower decimals.
568580 decimals_i = headers_can .index ( can ( '_Decimals' ))
569581 decimals = max ( line [decimals_i ] for page in pages for line in page )
570582 floatfmt = f',.{ decimals } f'
@@ -582,8 +594,15 @@ def can( c ):
582594 first = page_prev is None
583595 final = p + 1 == len ( pages )
584596
597+ # {Sub}total for payment cryptocurrrencies are rounded to the individual
598+ # Cryptocurrency's designated decimals // 3. For typical ERC-20 tokens, this is
599+ # eg. USDC: 6 // 3 == 2, WBTC: 18 // 3 == 6. For known Cryptocurrencies, eg. BTC: 24 //
600+ # 3 == 8. TODO: There should be a more sensible / less brittle way to do this.
585601 def deci ( c ):
586- return self .currencies_proxy [c ].decimals // 3
602+ try :
603+ return cryptocurrency_known ( c ).decimals // 3
604+ except Exception :
605+ return self .currencies_proxy [c ].decimals // 3
587606
588607 def toti ( c ):
589608 return headers_can .index ( can ( f'_Total { c } ' ))
@@ -599,20 +618,20 @@ def taxi( c ):
599618 c ,
600619 self .currencies_account [c ].name if c == self .currencies_account [c ].symbol else self .currencies_proxy [c ].name ,
601620 ] + [
602- round ( page [- 1 ][toti ( c )], deci ( c )),
603621 round ( page [- 1 ][taxi ( c )], deci ( c )),
622+ round ( page [- 1 ][toti ( c )], deci ( c )),
604623 ] if first else [
605- round ( page [- 1 ][toti ( c )] - page_prev [- 1 ][toti ( c )], deci ( c )),
606624 round ( page [- 1 ][taxi ( c )] - page_prev [- 1 ][taxi ( c )], deci ( c )),
625+ round ( page [- 1 ][toti ( c )] - page_prev [- 1 ][toti ( c )], deci ( c )),
607626 ]
608627 for c in sorted ( self .currencies )
609628 ],
610629 headers = (
611630 'Account' ,
612631 'Crypto' ,
613632 'Currency' ,
633+ 'Taxes' if final else f'Taxes { p + 1 } /{ len ( pages )} ' ,
614634 'Subtotal' if final else f'Subtotal { p + 1 } /{ len ( pages )} ' ,
615- 'Taxes' if final else f'Taxes { p + 1 } /{ len ( pages )} '
616635 ),
617636 intfmt = ',' ,
618637 floatfmt = ',g' ,
@@ -625,17 +644,17 @@ def taxi( c ):
625644 self .currencies_account [c ].address ,
626645 c ,
627646 self .currencies_account [c ].name if c == self .currencies_account [c ].symbol else self .currencies_proxy [c ].name ,
628- round ( page [- 1 ][toti ( c )], deci ( c )),
629647 round ( page [- 1 ][taxi ( c )], deci ( c )),
648+ round ( page [- 1 ][toti ( c )], deci ( c )),
630649 ]
631650 for c in sorted ( self .currencies )
632651 ],
633652 headers = (
634653 'Account' ,
635654 'Crypto' ,
636655 'Currency' ,
656+ 'Taxes' if final else f'Taxes { p + 1 } /{ len ( pages )} ' ,
637657 'Total' if final else f'Total { p + 1 } /{ len ( pages )} ' ,
638- 'Taxes' if final else f'Taxes { p + 1 } /{ len ( pages )} '
639658 ),
640659 intfmt = ',' ,
641660 floatfmt = ',g' ,
@@ -680,7 +699,7 @@ def layout_invoice(
680699
681700 """
682701 prio_backing = - 3
683- prio_normal = - 2 # noqa: F841
702+ prio_normal = - 2
684703 prio_contrast = - 1
685704
686705 if inv_margin is None :
@@ -690,7 +709,7 @@ def layout_invoice(
690709 inv_interior = inv .add_region_relative (
691710 Region ( 'inv-interior' , x1 = + inv_margin , y1 = + inv_margin , x2 = - inv_margin , y2 = - inv_margin )
692711 ).add_region_proportional (
693- # inv-background -- A full background image, out to the margins
712+ # inv-image -- A full background image, out to the margins; bottom-most in stack
694713 Image (
695714 'inv-image' ,
696715 priority = prio_backing ,
@@ -703,16 +722,20 @@ def layout_invoice(
703722 β = math .atan ( b / a ) # noqa: F841
704723 rotate = 90 - math .degrees ( β ) # noqa: F841
705724
706- inv_top = inv_interior .add_region_proportional (
707- Region ( 'inv-top' , x1 = 0 , y1 = 0 , x2 = 1 , y2 = 1 / 6 )
708- ).add_region_proportional (
725+ head = 25 / 100 # 25% header, 70% body
726+ foot = 95 / 100 # 5% footer
727+
728+ # inv-head: Image for header of Invoice (if no inv-image)
729+ inv_head = inv_interior .add_region_proportional (
709730 Image (
710- 'inv-top-bg' ,
711- priority = prio_backing ,
731+ 'inv-head' ,
732+ y2 = head ,
733+ priority = prio_normal ,
712734 ),
713735 )
714736
715- inv_top .add_region_proportional (
737+ # inv-label: Label "Invoice"
738+ inv_head .add_region_proportional (
716739 Image (
717740 'inv-label-bg' ,
718741 x1 = 0 ,
@@ -728,20 +751,40 @@ def layout_invoice(
728751 text = "Invoice" ,
729752 )
730753 )
731- inv_body = inv_interior .add_region_proportional (
732- Region ( 'inv-body' , x1 = 0 , y1 = 1 / 6 , x2 = 1 , y2 = 1 )
733- )
734754
735- inv_body .add_region_proportional (
755+ # inv-body, ...: Image for Body of Invoice;
756+ # inv-table: Main Invoice text area
757+ inv_interior .add_region_proportional (
758+ Image (
759+ 'inv-body' ,
760+ y1 = head ,
761+ y2 = foot ,
762+ priority = prio_normal ,
763+ )
764+ ).add_region_proportional (
765+ Image (
766+ 'inv-table-bg' ,
767+ priority = prio_contrast ,
768+ ),
769+ ).add_region_proportional (
736770 Text (
737771 'inv-table' ,
738772 y2 = 1 / rows ,
739773 font = 'mono' ,
740- bold = True ,
741774 size_ratio = 9 / 16 ,
742775 multiline = True ,
743776 )
744777 )
778+
779+ # inv-foot: Image for bottom of Invoice (if no inv-image)
780+ inv_interior .add_region_proportional (
781+ Image (
782+ 'inv-foot' ,
783+ y1 = foot ,
784+ priority = prio_normal ,
785+ ),
786+ )
787+
745788 return inv
746789
747790
@@ -905,7 +948,8 @@ def produce_invoice(
905948 last = i + 1 == len ( details )
906949 inv_tpl ['inv-image' ] = inv_image
907950 inv_tpl ['inv-table' ] = f"{ tbl } \n \n { 80 * ( '=' if last else '-' )} \n { tot if last else sub } "
908- inv_tpl ['inv-top-bg' ] = layout / '1x1-ffffff54.png'
951+ inv_tpl ['inv-label-bg' ] = layout / '1x1-ffffffbf.png'
952+ inv_tpl ['inv-table-bg' ] = layout / '1x1-ffffffbf.png'
909953
910954 inv_tpl .render ( offsetx = offsetx , offsety = offsety )
911955 # Caller already has the Invoice; return the PDF and computed InvoiceMetadata
0 commit comments