@@ -490,6 +490,12 @@ def conversions_candidates():
490490 self .conversions = conversions # { ("BTC","ETH"): 14.3914, ... }
491491 self .created = datetime .utcnow ().astimezone ( timezone .utc )
492492
493+ def decimals ( self , currency ):
494+ info = self .currencies_proxy [currency ]
495+ if info .symbol in self .currencies_alias :
496+ info = self .currencies_alias [info .symbol ]
497+ return info .decimals
498+
493499 def headers ( self ):
494500 """Output the headers to use in tabular formatting of iterator. By default, we'd recommend hiding any starting
495501 with an _ prefix (this is the default)."""
@@ -519,19 +525,19 @@ def __iter__( self ):
519525 """Iterate of lines, computing totals in each Invoice.currencies
520526
521527 Note that numeric values may be int, float or Fraction, and these are retained through
522- normal mathematical operations.
528+ normal mathematical operations, at full available precision (even if this is beyond the
529+ "decimals" precision of the underlying cryptocurrency).
530+
531+ The number of decimals desired for each line is provided in "_Decimals". The native
532+ number of decimals must be respected in this line's calculations. For example, if eg. a
533+ 0-decimals Token is used, the Net, Tax and Amount are rounded to 0 decimals. All
534+ rounding/display decimals should be performed at the final display of the data.
523535
524- The number of decimals desired for each line is provided in "_Decimals". The native number
525- of decimals must be respected in this line's calculations. For example, if eg. a
526- 0-decimals Token is used, the Net, Tax and Amount are rounded to 0 decimals. The sums in
527- various currencies converted from each line are not rounded; the currency ratio and hence
528- sum is full precision, and is only rounded at the final use.
536+ The sums in various currencies converted from each line are not rounded; the currency
537+ ratio and hence sum is full precision, and is only rounded at the final use.
529538
530539 """
531540
532- # Get all the eg. wBTC / USDC value ratios for the Line-item currency, vs. each Invoice
533- # currency at once, in case the iterator takes considerable time; we don't want to risk that
534- # memoization "refresh" the token values while generating the invoice!
535541 tot = {
536542 c : 0. for c in self .currencies
537543 }
@@ -540,17 +546,22 @@ def __iter__( self ):
540546 }
541547
542548 for i ,line in enumerate ( self .lines ):
543- line_currency = line .currency or INVOICE_CURRENCY
544549 line_amount ,line_taxes ,line_taxinf = line .net ()
550+ line_net = line_amount - line_taxes
545551 # The line's ERC-20 Token (or a known proxy for the named Cryptocurrency), or a
546- # TokenInfo for the specified known Cryptocurrency.
547- line_curr = cryptocurrency_proxy ( line_currency , w3_url = self .w3_url , use_provider = self .use_provider )
552+ # TokenInfo for the specified known Cryptocurrency. Always displays the underlying
553+ # native Cryptocurrency symbol (if known), even if a "Proxy" is specified, or used
554+ # for pricing.
555+ line_currency = line .currency or INVOICE_CURRENCY # Eg. "Bitcoin", "WBTC", "BTC"
556+ line_curr = self .currencies_proxy [line_currency ]
557+ log .info ( f"Line currency { line_currency } has proxy { line_curr .symbol :6} : decimals: { line_curr .decimals } " )
558+ if line_curr .symbol in self .currencies_alias :
559+ line_curr = self .currencies_alias [line_curr .symbol ]
560+ log .info ( f"Line currency { line_currency } alias for { line_curr .symbol :6} : decimals: { line_curr .decimals } " )
548561 line_symbol = line_curr .symbol
549- line_decimals = line_curr .decimals // 3 if line .decimals is None else line .decimals
550-
551- line_amount = round ( line_amount , line_decimals )
552- line_taxes = round ( line_taxes , line_decimals )
553- line_net = line_amount - line_taxes
562+ line_decimals = line .decimals
563+ if line_decimals is None :
564+ line_decimals = line_curr .decimals // 3
554565
555566 for c in self .currencies :
556567 if line_curr .symbol == c :
@@ -689,9 +700,7 @@ def can( c ):
689700 #
690701 # TODO: There should be a more sensible / less brittle way to do this.
691702 def deci ( c ):
692- if c in self .currencies_alias :
693- return self .currencies_alias [c ].decimals // 3
694- return self .currencies_proxy [c ].decimals // 3
703+ return self .decimals ( c ) // 3
695704
696705 def toti ( c ):
697706 return headers_can .index ( can ( f'_Total { c } ' ))
@@ -750,8 +759,18 @@ def taxi( c ):
750759 )
751760
752761 def fmt ( val , hdr , coin ):
753- if can ( hdr ) in ( 'price' , 'taxes' , 'amount' ):
762+ hdr_can = can ( hdr )
763+ if hdr_can in ( 'price' , 'taxes' , 'amount' , 'net' ):
764+ # It's a Price, or a computed Taxes/Amount/Net; use the line's cryptocurrency to round
754765 return round ( val , deci ( coin ))
766+ try :
767+ t ,t_coin = hdr .split ()
768+ except Exception :
769+ pass
770+ else :
771+ if can ( t ) in ('total' , 'taxes' ):
772+ # It's a 'Total/Taxes COIN' column; use the specified coin's decimals for rounding
773+ return round ( val , deci ( t_coin ))
755774 return val
756775
757776 # Produce the page line-items. We must round each line-item's numeric price values
@@ -1252,47 +1271,54 @@ def write_invoices(
12521271 invoices : Sequence [Tuple [Invoice ,InvoiceMetadata ]], # sequence of [ (Invoice, InvoiceMetadata), ... ] or { "<name>": Invoice, ... }
12531272 filename = True , # A file name/Path, if PDF output to file is desired; ''/True implies default., False no file
12541273 ** kwds
1255- ):
1274+ ) -> Union [ InvoiceOutput , Exception ] :
12561275 """Generate unique cryptocurrency account(s) for each client invoice, generate the invoice,
12571276 and (optionally) write them to files. Yields a sequence of the generated invoice PDF names
12581277 and details.
12591278
1279+ If an Exception is raised during Invoice generation, a (None, <Exception>) is generated,
1280+ instead of a ("name", <InvoiceOutput).
1281+
12601282 """
12611283 if filename is None :
12621284 filename = True
12631285 for invoice ,metadata in invoices :
1264- # Provides the supplied invoice,metadata, and receives the transformed (specialized) metadata.
1265- _ ,pdf ,metadata = produce_invoice (
1266- invoice = invoice ,
1267- metadata = metadata ,
1268- ** kwds
1269- )
1286+ try :
1287+ # Provides the supplied invoice,metadata, and receives the transformed (specialized) metadata.
1288+ _ ,pdf ,metadata = produce_invoice (
1289+ invoice = invoice ,
1290+ metadata = metadata ,
1291+ ** kwds
1292+ )
12701293
1271- accounts = invoice .accounts
1272- assert accounts , \
1273- "At least one Cryptocurrency account must be specified"
1294+ accounts = invoice .accounts
1295+ assert accounts , \
1296+ "At least one Cryptocurrency account must be specified"
12741297
1275- pdf_name = (( '' if filename is True else filename ) or FILENAME_FORMAT ).format (
1276- name = metadata .number ,
1277- date = datetime .strftime ( metadata .date , '%Y-%m-%d' ),
1278- time = datetime .strftime ( metadata .date , '%H.%M.%S' ),
1279- crypto = accounts [0 ].crypto ,
1280- address = accounts [0 ].address ,
1281- )
1282- if not pdf_name .lower ().endswith ( '.pdf' ):
1283- pdf_name += '.pdf'
1284- log .warning ( f"Invoice { metadata .number } : { pdf_name } " )
1285-
1286- path = None
1287- if filename is not False :
1288- path = metadata .directory .resolve () / pdf_name
1289- log .warning ( f"Writing Invoice { metadata .number !r} to: { path } " )
1290- pdf .output ( path )
1291-
1292- invoice_output = InvoiceOutput (
1293- invoice = invoice ,
1294- metadata = metadata ,
1295- pdf = pdf ,
1296- path = path ,
1297- )
1298- yield pdf_name , invoice_output
1298+ name = (( '' if filename is True else filename ) or FILENAME_FORMAT ).format (
1299+ name = metadata .number ,
1300+ date = datetime .strftime ( metadata .date , '%Y-%m-%d' ),
1301+ time = datetime .strftime ( metadata .date , '%H.%M.%S' ),
1302+ crypto = accounts [0 ].crypto ,
1303+ address = accounts [0 ].address ,
1304+ )
1305+ if not name .lower ().endswith ( '.pdf' ):
1306+ name += '.pdf'
1307+ log .warning ( f"Invoice { metadata .number } : { name } " )
1308+
1309+ path = None
1310+ if filename is not False :
1311+ path = metadata .directory .resolve () / name
1312+ log .warning ( f"Writing Invoice { metadata .number !r} to: { path } " )
1313+ pdf .output ( path )
1314+
1315+ output = InvoiceOutput (
1316+ invoice = invoice ,
1317+ metadata = metadata ,
1318+ pdf = pdf ,
1319+ path = path ,
1320+ )
1321+ except Exception as exc :
1322+ name , output = None , exc
1323+
1324+ yield name , output
0 commit comments