USt-Berechnung und Zwischenspeicherung komplett umgestellt. VATEX-Code für innergemeinschaftliche Lieferung im XML
Bernd Wurst

Bernd Wurst commited on 2025-05-03 07:43:46
Zeige 6 geänderte Dateien mit 69 Einfügungen und 52 Löschungen.

... ...
@@ -24,7 +24,7 @@ class InvoiceText(object):
24 24
 
25 25
 
26 26
 class InvoiceTable(object):
27
-    def __init__(self, vatType='gross', tender=False, summary=True):
27
+    def __init__(self, vatType='net', tender=False, summary=True):
28 28
         self.entries = []
29 29
         self.vat = {}
30 30
         self.sum = Decimal('0.0')
... ...
@@ -43,11 +43,15 @@ class InvoiceTable(object):
43 43
         e = entry
44 44
         if not ('count' in k and 'unit' in k and 'subject' in k and 'price' in k and 'vat' in k):
45 45
             raise ValueError('Some data is missing!')
46
+        # Finde heraus, wie viele Dezimalstellen übergeben wurden, minimum aber 2.
47
+        digits = max(2, abs(Decimal(f"{e['price']}").as_tuple().exponent))
46 48
         ret = {'type': 'entry',
47
-               'count': e['count'],
49
+               'count': Decimal(f"{e['count']:.4f}"),
48 50
                'unit': e['unit'],
49 51
                'subject': e['subject'],
50
-               'price': e['price'],
52
+               'net_price': round(e['price'] if self.vatType == 'net' else e['price'] / Decimal(f"{e['vat'] + 1:.2f}"), digits),
53
+               'gross_price': round(e['price'] * (e['vat'] + 1) if self.vatType == 'net' else e['price'], digits),
54
+               'price': Decimal(e['price']),
51 55
                'total': e['price'] * e['count'],
52 56
                'vat': Decimal(e['vat']),
53 57
                'tender': False,
... ...
@@ -74,9 +78,14 @@ class InvoiceTable(object):
74 78
         Typen sein"""
75 79
         d = self.validEntry(data)
76 80
         if not d['vat'] in self.vat.keys():
77
-            self.vat[d['vat']] = [0, chr(65+len(self.vat))]
81
+            self.vat[d['vat']] = {'char': chr(65+len(self.vat)), 'net': Decimal("0.00"),
82
+                                  'vat_amount': Decimal("0.00"), 'gross': Decimal("0.00"),
83
+                                  'vat': d['vat'],}
78 84
         if 'tender' not in data or not data['tender']:
79
-            self.vat[d['vat']][0] += d['total']
85
+            self.vat[d['vat']]['net'] += d['net_price'] * d['count']
86
+            self.vat[d['vat']]['gross'] += d['gross_price'] * d['count']
87
+            # vat wurde bereits vereinheitlicht auf einen Dezimalfaktor
88
+            self.vat[d['vat']]['vat_amount'] += d['net_price'] * d['count'] * d['vat']
80 89
             self.sum += d['total']
81 90
         self.entries.append(d)
82 91
     
... ...
@@ -9,7 +9,7 @@ from reportlab.pdfbase.ttfonts import TTFont, TTFError
9 9
 from reportlab.pdfgen import canvas
10 10
 
11 11
 from . import _formatPrice
12
-from .InvoiceObjects import InvoiceTable, InvoiceText, InvoiceImage, GUTSCHRIFT, KORREKTUR
12
+from .InvoiceObjects import InvoiceTable, InvoiceText, InvoiceImage, GUTSCHRIFT, KORREKTUR, VAT_INNERGEM
13 13
 
14 14
 
15 15
 def find_font_file(filename):
... ...
@@ -444,7 +444,7 @@ class PDF(object):
444 444
                             self.canvas.drawRightString(left + 13.3 * cm, self.y - self.font_height,
445 445
                                                         _formatPrice(el['price']))
446 446
                             self.canvas.drawString(left + 13.7 * cm, self.y - self.font_height,
447
-                                                   str(part.vat[el['vat']][1]))
447
+                                                   str(part.vat[el['vat']]['char']))
448 448
                         if el['tender']:
449 449
                             self.canvas.drawRightString(left + 16.8 * cm, self.y - self.font_height, 'eventual')
450 450
                         else:
... ...
@@ -493,31 +493,39 @@ class PDF(object):
493 493
                             self.canvas.setFont(self.font, font_size)
494 494
                             self.y -= self.line_height + self.line_padding
495 495
                             summaries = []
496
-                            vat = Decimal('0.0')
497
-                            if len(part.vat) == 1 and list(part.vat.keys())[0] == 0.0:
496
+                            netsum = Decimal('0.0')
497
+                            if len(part.vat) == 1 and Decimal("0.0") in part.vat:
498
+                                vat = list(part.vat.values())[0]
499
+                                netsum += vat['net']
500
+                                if self.invoice.vat_type == VAT_INNERGEM:
501
+                                    self.canvas.drawString(left, self.y - self.font_height,
502
+                                                           'Innergemeinschaftliche Lieferung ohne USt.')
503
+                                else:
498 504
                                     self.canvas.drawString(left, self.y - self.font_height,
499 505
                                                            'Dieser Beleg wurde ohne Ausweis von MwSt erstellt.')
500 506
                                 self.y -= self.line_height
501 507
                             else:
502 508
                                 if len(part.vat) == 1:
503
-                                    vat = list(part.vat.keys())[0]
509
+                                    vat = list(part.vat.values())[0]
510
+                                    netsum += vat['net']
504 511
                                     if self.invoice.tender:
505
-                                        summaries.append(('Im Gesamtbetrag sind %.1f%% MwSt enthalten:' % (vat * 100),
506
-                                                          _formatPrice((part.sum / (vat + 1)) * vat)))
512
+                                        summaries.append(('Im Gesamtbetrag sind %.1f%% MwSt enthalten:' % (vat['vat'] * 100),
513
+                                                          _formatPrice(vat['vat_amount'])))
507 514
                                     else:
508 515
                                         summaries.append((
509
-                                            'Im Rechnungsbetrag sind %.1f%% MwSt enthalten:' % (vat * 100),
510
-                                            _formatPrice((part.sum / (vat + 1)) * vat)))
516
+                                            'Im Rechnungsbetrag sind %.1f%% MwSt enthalten:' % (vat['vat'] * 100),
517
+                                            _formatPrice(vat['vat_amount'])))
511 518
                                 else:
512
-                                    for vat, vatdata in part.vat.items():
513
-                                        if vat > 0:
519
+                                    for vat in part.vat.values():
520
+                                        netsum += vat['net']
521
+                                        if vat['vat'] > 0:
514 522
                                             summaries.append(('%s: Im Teilbetrag von %s sind %.1f%% MwSt enthalten:' % (
515
-                                                vatdata[1], _formatPrice(vatdata[0]), vat * 100),
516
-                                                              _formatPrice((vatdata[0] / (vat + 1)) * vat)))
523
+                                                vat['char'], _formatPrice(vat['gross']), vat['vat'] * 100),
524
+                                                              _formatPrice(vat['vat_amount'])))
517 525
                                         else:
518 526
                                             summaries.append(('%s: Durchlaufende Posten ohne Berechnung von MwSt.' % (
519
-                                                vatdata[1]), Decimal('0.0')))
520
-                            summaries.append(('Nettobetrag:', _formatPrice(part.sum - (part.sum / (vat + 1)) * vat)))
527
+                                                vat['char']), Decimal('0.0')))
528
+                            summaries.append(('Nettobetrag:', _formatPrice(netsum)))
521 529
                             summaries.sort()
522 530
                             for line in summaries:
523 531
                                 self.canvas.drawRightString(left + 11.5 * cm, self.y - self.font_height, '('+line[0])
... ...
@@ -529,7 +537,12 @@ class PDF(object):
529 537
                         self.canvas.drawRightString(left + 16.8 * cm, self.y - self.font_height, _formatPrice(part.sum))
530 538
                         self.y -= self.line_height
531 539
                         summaries = []
532
-                        if list(part.vat.keys())[0] == 0.0:
540
+                        if len(part.vat) == 1 and Decimal("0.0") in part.vat:
541
+                            vat = list(part.vat.values())[0]
542
+                            if self.invoice.vat_type == VAT_INNERGEM:
543
+                                self.canvas.drawString(left, self.y - self.font_height,
544
+                                                       'Innergemeinschaftliche Lieferung ohne USt.')
545
+                            else:
533 546
                                 self.canvas.drawString(left, self.y - self.font_height,
534 547
                                                        'Dieser Beleg wurde ohne Ausweis von MwSt erstellt.')
535 548
                             self.y -= self.line_height
... ...
@@ -538,17 +551,17 @@ class PDF(object):
538 551
                                 vat = list(part.vat.keys())[0]
539 552
                                 summaries.append(('zzgl. %.1f%% MwSt:' % (vat * 100), _formatPrice(vat * part.sum)))
540 553
                             else:
541
-                                for vat, vatdata in part.vat.items():
542
-                                    summaries.append(('zzgl. %.1f%% MwSt (%s):' % (vat * 100, vatdata[1]),
543
-                                                      _formatPrice(vat * vatdata[0])))
554
+                                for vat in part.vat.values():
555
+                                    summaries.append(('zzgl. %.1f%% MwSt (%s):' % (vat['vat'] * 100, vat['char']),
556
+                                                      _formatPrice(vat['vat_amount'])))
544 557
                         summaries.sort()
545 558
                         for line in summaries:
546 559
                             self.canvas.drawRightString(left + 14.5 * cm, self.y - self.font_height, line[0])
547 560
                             self.canvas.drawRightString(left + 16.8 * cm, self.y - self.font_height, line[1])
548 561
                             self.y -= self.line_height
549 562
                         sum = Decimal('0.0')
550
-                        for vat, vatdata in part.vat.items():
551
-                            sum += (vat + 1) * vatdata[0]
563
+                        for vat in part.vat.values():
564
+                            sum += vat['gross']
552 565
                         self.canvas.setFont(self.font + '-Bold', font_size)
553 566
                         if self.invoice.tender:
554 567
                             self.canvas.drawRightString(left + 14.5 * cm, self.y - self.font_height, 'Gesamtbetrag:')
... ...
@@ -578,8 +591,8 @@ class PDF(object):
578 591
                     sum = part.sum
579 592
                     if part.vatType == 'net':
580 593
                         sum = Decimal('0.0')
581
-                        for vat, vatdata in part.vat.items():
582
-                            sum += (vat + 1) * vatdata[0]
594
+                        for vatdata in part.vat.values():
595
+                            sum += vatdata['gross']
583 596
                     rest = sum - paysum
584 597
                     if part.payments and rest > 0:
585 598
                         self.canvas.setFont(self.font + '-Bold', font_size)
... ...
@@ -98,7 +98,7 @@ def InvoiceTableToText(invoiceTable: InvoiceTable):
98 98
         else:
99 99
             subject = _breakLine(el['subject'], width=41)
100 100
             ret.append(u'%5.2f %-3s %-37s  %10s %s %10s' % (
101
-                el['count'], unit, subject[0], _formatPrice(el['price']), invoiceTable.vat[el['vat']][1],
101
+                el['count'], unit, subject[0], _formatPrice(el['price']), invoiceTable.vat[el['vat']]['char'],
102 102
                 _formatPrice(el['total'])))
103 103
             for i in range(1, len(subject)):
104 104
                 ret.append(u'          %s' % subject[i])
... ...
@@ -122,13 +122,13 @@ def InvoiceTableToText(invoiceTable: InvoiceTable):
122 122
         ret.append('')
123 123
         summaries = []
124 124
         if len(invoiceTable.vat) == 1:
125
-            vat = list(invoiceTable.vat.keys())[0]
125
+            vat = list(invoiceTable.vat.values())[0]
126 126
             summaries.append(u'      Im Rechnungsbetrag sind %.1f%% MwSt enthalten (%s)' % (
127
-                vat * 100, _formatPrice((invoiceTable.sum / (vat + 1)) * vat)))
127
+                vat['vat'] * 100, _formatPrice(vat['vat_amount'])))
128 128
         else:
129
-            for vat, vatdata in list(invoiceTable.vat.items()):
129
+            for vat in invoiceTable.vat.values():
130 130
                 summaries.append(u'    %s: Im Teilbetrag von %s sind %.1f%% MwSt enthalten (%s)' % (
131
-                    vatdata[1], _formatPrice(vatdata[0]), vat * 100, _formatPrice((vatdata[0] / (vat + 1)) * vat)))
131
+                    vat['char'], _formatPrice(vat['gross']), vat['vat'] * 100, _formatPrice(vat['vat_amount'])))
132 132
         summaries.sort()
133 133
         for line in summaries:
134 134
             ret.append(line)
... ...
@@ -136,19 +136,19 @@ def InvoiceTableToText(invoiceTable: InvoiceTable):
136 136
         ret.append((u'Nettobetrag: %11s' % _formatPrice(invoiceTable.sum)).rjust(72))
137 137
         summaries = []
138 138
         if len(invoiceTable.vat) == 1:
139
-            vat = list(invoiceTable.vat.keys())[0]
140
-            summaries.append((u'zzgl. %.1f%% MwSt: %11s' % (vat * 100, _formatPrice(vat * invoiceTable.sum))).rjust(72))
139
+            vat = list(invoiceTable.vat.values())[0]
140
+            summaries.append((u'zzgl. %.1f%% MwSt: %11s' % (vat['vat'] * 100, _formatPrice(vat['vat_amount']))).rjust(72))
141 141
         elif len(invoiceTable.vat) > 1:
142
-            for vat, vatdata in list(invoiceTable.vat.items()):
142
+            for vat in invoiceTable.vat.values():
143 143
                 summaries.append(
144
-                    (u'zzgl. %4.1f%% MwSt (%s): %11s' % (vat * 100, vatdata[1], _formatPrice(vat * vatdata[0]))).rjust(
144
+                    (u'zzgl. %4.1f%% MwSt (%s): %11s' % (vat['vat'] * 100, vat['char'], _formatPrice(vat['vat_amount']))).rjust(
145 145
                         72))
146 146
         summaries.sort()
147 147
         for line in summaries:
148 148
             ret.append(line)
149 149
         tablesum = invoiceTable.sum
150
-        for vat, vatdata in list(invoiceTable.vat.items()):
151
-            tablesum += vat * vatdata[0]
150
+        for vat in invoiceTable.vat.values():
151
+            tablesum += vat['vat_amount']
152 152
         ret.append((u'Rechnungsbetrag: %11s' % _formatPrice(tablesum)).rjust(72))
153 153
     ret.append('')
154 154
     return '\n'.join(ret)
... ...
@@ -246,14 +246,10 @@ def InvoiceToXML(invoice):
246 246
             for vat, vatdata in part.vat.items():
247 247
                 trade_tax = ApplicableTradeTax()
248 248
                 # Steuerbetrag dieses Steuersatzes
249
-                amount = vatdata[0] * vat
250
-                if part.vatType == 'gross':
251
-                    amount = (vatdata[0] / (vat + 1)) * vat
249
+                amount = vatdata['vat_amount']
252 250
                 trade_tax.calculated_amount = Decimal(f"{amount:.2f}")
253 251
                 # Nettosumme dieses Steuersatzes
254
-                amount = vatdata[0]
255
-                if part.vatType == 'gross':
256
-                    amount = amount / (1 + vat)
252
+                amount = vatdata['net']
257 253
                 trade_tax.basis_amount = Decimal(f"{amount:.2f}")
258 254
                 trade_tax.type_code = "VAT"
259 255
                 if invoice.vat_type == VAT_REGULAR:
... ...
@@ -263,11 +259,10 @@ def InvoiceToXML(invoice):
263 259
                     trade_tax.exemption_reason = 'Als Kleinunternehmer wird gemäß §19 UStG keine USt in Rechnung gestellt.'
264 260
                 elif invoice.vat_type == VAT_INNERGEM:
265 261
                     trade_tax.category_code = "K"
262
+                    trade_tax.exemption_reason_code = 'vatex-eu-ic'
263
+                    trade_tax.exemption_reason = 'Innergemeinschaftliche Lieferung ohne Berechnung von Umsatzsteuer.'
266 264
                 trade_tax.rate_applicable_percent = Decimal(f"{vat * 100:.1f}")
267
-                if part.vatType == 'gross':
268
-                    summe_ust += (vatdata[0] / (vat + 1)) * vat
269
-                else:
270
-                    summe_ust += vatdata[0] * vat
265
+                summe_ust += vatdata['vat_amount']
271 266
                 doc.trade.settlement.trade_tax.add(trade_tax)
272 267
 
273 268
     for paragraph in textparts:
... ...
@@ -12,7 +12,7 @@ import qrcode
12 12
 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
13 13
 
14 14
 from Invoice.InvoiceObjects import Invoice, RECHNUNG, InvoiceText, InvoiceTable, KORREKTUR, PAYMENT_LASTSCHRIFT, \
15
-    InvoiceImage
15
+    InvoiceImage, VAT_INNERGEM
16 16
 from Invoice.InvoiceToPDF import InvoiceToPDF, find_font_file
17 17
 from Invoice.InvoiceToText import InvoiceToText
18 18
 from Invoice.InvoiceToZUGFeRD import InvoiceToXML, attach_xml
... ...
@@ -17,13 +17,13 @@ data = {
17 17
  'data': ({'anzahl': Decimal('12.00'),
18 18
            'beschreibung': 'Testposten',
19 19
            'betrag': Decimal('8'),
20
-           'brutto': 1,
20
+           'brutto': 0,
21 21
            'datum': datetime.date(2023, 9, 20),
22 22
            'einheit': 'Stk',
23 23
            'enddatum': None,
24 24
            'info_headline': None,
25 25
            'info_text': None,
26
-           'mwst': 19.0},),
26
+           'mwst': Decimal('19.0')},),
27 27
  'daten': {'kuendigung': datetime.date(2024, 2, 16),
28 28
            'kunde': 5,
29 29
            'naechster_kuendigungstermin': datetime.date(2024, 3, 1),
30 30