@@ -131,6 +131,37 @@ def _read_xlsx_sheet_columns(
131
131
132
132
return target
133
133
134
+ def _harmonise_imported_data (self ):
135
+ """Method to cast the data type of numerical data to `decimal.Decimal`,
136
+ and, the data of type `datetime.datetime` to ISO formatted date
137
+ strings. This method affects private members `self._invoice` and
138
+ `self._invoice_lines`.
139
+
140
+ Invoice (line) monetary amounts are always rounded to two decimal places.
141
+ Item price figures can have more that two decimal figures and should be
142
+ represented by decimal numbers with at least two decimal figures.
143
+ """
144
+
145
+ decimal .getcontext ().rounding = decimal .ROUND_HALF_UP
146
+
147
+ amount_keyword_pattern = re .compile (r"^xr\:[A-Za-z0-9\_]+_amount$" )
148
+ price_keyword_pattern = re .compile (r"^xr\:[A-Za-z0-9\_]+_price$" )
149
+ other_decimal_keyword_pattern = re .compile (
150
+ r"^xr\:[A-Za-z0-9\_]+_VAT_rate$|^xr\:[A-Za-z0-9\_]+_quantity$"
151
+ )
152
+ for d in [self ._invoice ] + self ._invoice_lines :
153
+ for k , v in d .items ():
154
+ if type (v ) == datetime .datetime :
155
+ d [k ] = v .date ().isoformat ()
156
+ elif amount_keyword_pattern .match (k ):
157
+ d [k ] = round (decimal .Decimal (v ), 2 )
158
+ elif price_keyword_pattern .match (k ):
159
+ d [k ] = decimal .Decimal (v )
160
+ if d [k ].as_tuple ().exponent > - 2 :
161
+ d [k ] = round (d [k ], 2 )
162
+ elif other_decimal_keyword_pattern .match (k ):
163
+ d [k ] = decimal .Decimal (v )
164
+
134
165
def _calculate_tax_total (self ):
135
166
"""From list of dictionaries `self._invoice_lines` determine the list of
136
167
dictionaries `self._tax_subtotals`, and, add key 'xr:Invoice_total_VAT_amount'
@@ -196,7 +227,7 @@ def _calculate_monetary_total(self):
196
227
assert "xr:Sum_of_Invoice_line_net_amount" in self ._invoice
197
228
assert "xr:Invoice_total_VAT_amount" in self ._invoice
198
229
199
- # BT-106 Summe der Nettobeträge aller Rechnungspositionen"
230
+ # BT-106 Summe der Nettobeträge aller Rechnungspositionen
200
231
sum_of_invoice_line_net_amounts = decimal .Decimal (
201
232
self ._invoice .get ("xr:Sum_of_Invoice_line_net_amount" )
202
233
)
@@ -361,7 +392,7 @@ def _prepare_xrechnung(self) -> list:
361
392
# ---------- ---------- public methods ---------- ----------
362
393
363
394
def read_xlsx (self , workbook_file : Path ):
364
- """Obtain invoice data from an XLSX file that has the required
395
+ """Obtain invoice data from an XLSX workbook file, which has the required
365
396
format (see the corresponding template file).
366
397
"""
367
398
@@ -387,18 +418,10 @@ def read_xlsx(self, workbook_file: Path):
387
418
388
419
# (1) general invoice data
389
420
if sheet_name == "Invoice" :
390
- assert "xr:Invoice_currency_code" in sheet_data .index
391
421
assert "Value" in wb [sheet_name ].columns
392
422
sheet_data = sheet_data [sheet_data .Value .notna ()]
393
423
self ._invoice = sheet_data ["Value" ].to_dict ()
394
-
395
- decimal .getcontext ().rounding = decimal .ROUND_HALF_UP
396
- for k , v in self ._invoice .items ():
397
- if self ._amount_keyword_pattern .match (k ):
398
- self ._invoice [k ] = round (decimal .Decimal (v ), 2 )
399
- if type (v ) == datetime .datetime :
400
- self ._invoice [k ] = v .date ().isoformat ()
401
- self ._currency_id = self ._invoice ["xr:Invoice_currency_code" ]
424
+ self ._currency_id = self ._invoice .get ("xr:Invoice_currency_code" , "EUR" )
402
425
403
426
# (2) invoice lines
404
427
elif sheet_name == "InvoiceLines" :
@@ -410,6 +433,7 @@ def read_xlsx(self, workbook_file: Path):
410
433
sheet_data , workbook_file .parent
411
434
)
412
435
436
+ self ._harmonise_imported_data ()
413
437
self ._calculate_tax_total ()
414
438
self ._calculate_monetary_total ()
415
439
@@ -427,21 +451,13 @@ def read_json(self, json_file: Path):
427
451
# (1) invoice currency
428
452
self ._currency_id = self ._invoice .get ("xr:Invoice_currency_code" , "EUR" )
429
453
430
- # (2) invoice- lines
454
+ # (2) invoice lines
431
455
self ._invoice_lines = [
432
456
{k : v for k , v in invoice_line .items () if v is not None }
433
457
for invoice_line in self ._invoice .pop ("InvoiceLines" , [])
434
458
]
435
459
self ._logger .info (f"found { len (self ._invoice_lines )} invoice line(s)" )
436
460
437
- decimal .getcontext ().rounding = decimal .ROUND_HALF_UP
438
- for invoice_line in self ._invoice_lines :
439
- for k , v in invoice_line .items ():
440
- if self ._amount_keyword_pattern .match (k ):
441
- invoice_line [k ] = round (decimal .Decimal (v ), 2 )
442
- elif self ._decimal_number_keyword_pattern .match (k ):
443
- invoice_line [k ] = decimal .Decimal (v )
444
-
445
461
# (3) additional document references
446
462
self ._additional_document_references = [
447
463
{k : v for k , v in adr .items () if v is not None }
@@ -463,6 +479,7 @@ def read_json(self, json_file: Path):
463
479
adr for adr in self ._additional_document_references if adr is not None
464
480
]
465
481
482
+ self ._harmonise_imported_data ()
466
483
self ._calculate_tax_total ()
467
484
self ._calculate_monetary_total ()
468
485
0 commit comments