Bernd Wurst

Bernd Wurst commited on 2024-01-25 07:38:14
Zeige 4 geänderte Dateien mit 1084 Einfügungen und 96 Löschungen.

... ...
@@ -0,0 +1,177 @@
1
+# -* coding: utf8 *-
2
+# (C) 2011 by Bernd Wurst <bernd@schokokeks.org>
3
+
4
+# This file is part of Bib2011.
5
+#
6
+# Bib2011 is free software: you can redistribute it and/or modify
7
+# it under the terms of the GNU General Public License as published by
8
+# the Free Software Foundation, either version 3 of the License, or
9
+# (at your option) any later version.
10
+#
11
+# Bib2011 is distributed in the hope that it will be useful,
12
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
+# GNU General Public License for more details.
15
+#
16
+# You should have received a copy of the GNU General Public License
17
+# along with Bib2011.  If not, see <http://www.gnu.org/licenses/>.
18
+
19
+import datetime
20
+
21
+class InvoiceImage(object):
22
+    def __init__(self, pilimage, caption=None, dpi=80, alignment="left"):
23
+        self.imagedata = pilimage
24
+        self.alignment = alignment
25
+        self.dpi = dpi
26
+        self.caption = caption
27
+
28
+
29
+class InvoiceText(object):
30
+    def __init__(self, content, urgent=False, headline=None):
31
+        self.paragraphs = [content]
32
+        self.urgent = urgent
33
+        self.headline = headline
34
+        self.fontsize = 0 # relative Schriftgröße ggü default
35
+
36
+    def addParagraph(self, content):
37
+        self.paragraphs.append(content)
38
+
39
+
40
+class InvoiceTable(object):
41
+    def __init__(self, vatType = 'gross', tender = False, summary = True):
42
+        self.entries = []
43
+        self.vat = {}
44
+        self.sum = 0.0
45
+        self.payments = []
46
+        self.tender = tender
47
+        self.summary = summary
48
+        if vatType not in ['gross', 'net']:
49
+            raise ValueError('vatType must be »gross« or »net«')
50
+        self.vatType = vatType
51
+    
52
+    def validEntry(self, entry):
53
+        '''bekommt einen Eintrag und liefert einen Eintrag wenn ok, wirft ansonsten ValueError.
54
+        wird benutzt um z.B. die Summe auszurechnen oder ähnliches
55
+        '''
56
+        k = entry.keys()
57
+        e = entry
58
+        if not ('count' in k and 'unit' in k and 'subject' in k and 'price' in k and 'vat' in k):
59
+            raise ValueError('Some data is missing!')
60
+        ret = {'type': 'entry',
61
+               'count': e['count'],
62
+               'unit': e['unit'],
63
+               'subject': e['subject'],
64
+               'price': e['price'],
65
+               'total': (e['price'] * e['count']),
66
+               'vat': e['vat'],
67
+               'tender': False,
68
+               }
69
+        if ret['vat'] > 1:
70
+            ret['vat'] = float(ret['vat']) / 100
71
+            
72
+        if 'tender' in e.keys():
73
+            ret['tender'] = e['tender']
74
+        if 'desc' in k:
75
+            ret['desc'] = e['desc']
76
+        if 'period_start' in k:
77
+            ret['period_start'] = e['period_start']
78
+        if 'period_end' in k:
79
+            ret['period_end'] = e['period_end']
80
+
81
+        return ret
82
+    
83
+    def addItem(self, data):
84
+        '''Fügt eine Zeile ein. data muss ein Dict mit passenden Keys und passenden
85
+        Typen sein'''
86
+        d = self.validEntry(data)
87
+        if not d['vat'] in self.vat.keys():
88
+            self.vat[d['vat']] = [0, chr(65+len(self.vat))]
89
+        if 'tender' not in data or not data['tender']:
90
+            self.vat[d['vat']][0] += d['total']
91
+            self.sum += d['total']
92
+        self.entries.append(d)
93
+    
94
+    def addTitle(self, title):
95
+        self.entries.append({'type': 'title', 'title': title,})
96
+
97
+    def addPayment(self, type, amount, date):
98
+        self.payments.append({"type": type, "amount": amount, "date": date})
99
+
100
+
101
+RECHNUNG = 380
102
+ANGEBOT = 1
103
+GUTSCHRIFT = 381
104
+KORREKTUR = 384
105
+
106
+VAT_REGULAR = 'S'
107
+VAT_KLEINUNTERNEHMER = 'E'
108
+VAT_INNERGEM = 'K'
109
+
110
+class Invoice(object):
111
+    def __init__(self, tender = False):
112
+        self.customerno = None
113
+        self.customer = {
114
+            "id": None,
115
+            "name": None,
116
+            "address": {
117
+                "postcode": None,
118
+                "city_name": None,
119
+                "line1": None,
120
+                "line2": None,
121
+                "line3": None,
122
+                "country_id": None,
123
+            },
124
+            "email": None,
125
+        }
126
+        self.buyer = self.customer
127
+        self.seller = {
128
+            "name": None, # juristischer Name
129
+            "trade_name": None, # Firmenname
130
+            "address": {
131
+                "postcode": None,
132
+                "city_name": None,
133
+                "line1": None,
134
+                "line2": None,
135
+                "line3": None,
136
+                "country_id": None,
137
+            },
138
+            "phone": None,
139
+            "email": None,
140
+            "website": None,
141
+        }
142
+        self.seller_vat_id = None
143
+        self.seller_bank_data = {
144
+            'kontoinhaber': None,
145
+            'iban': None,
146
+            'bic': None,
147
+            'bankname': None,
148
+        }
149
+        self.due_date = None
150
+        self.debit = False
151
+        self.debit_mandate_id = None
152
+        self.creditor_reference_id = None
153
+        self.buyer_bank_data = {
154
+            'kontoinhaber': None,
155
+            'iban': None,
156
+            'bic': None,
157
+            'bankname': None,
158
+        }
159
+        self.vat_type = VAT_REGULAR
160
+        self.salutation = 'Sehr geehte Damen und Herren,'
161
+        self.id = None
162
+        self.cash = True
163
+        self.type = RECHNUNG
164
+        self.logo_image_file = None
165
+        self.tender = tender
166
+        self.title = 'Rechnung'
167
+        if tender:
168
+            self.title = 'Angebot'
169
+        self.official = True
170
+        self.parts = []
171
+        self.pagecount = 0
172
+        self.date = datetime.date.today()
173
+    
174
+    def setDate(self, date):
175
+        if type(date) != datetime.date:
176
+            raise ValueError('date must be of type »datetime.date«')
177
+        self.date = date
... ...
@@ -0,0 +1,654 @@
1
+# -*- coding: utf-8 -*-
2
+# (C) 2011 by Bernd Wurst <bernd@schokokeks.org>
3
+
4
+# This file is part of Bib2011.
5
+#
6
+# Bib2011 is free software: you can redistribute it and/or modify
7
+# it under the terms of the GNU General Public License as published by
8
+# the Free Software Foundation, either version 3 of the License, or
9
+# (at your option) any later version.
10
+#
11
+# Bib2011 is distributed in the hope that it will be useful,
12
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
+# GNU General Public License for more details.
15
+#
16
+# You should have received a copy of the GNU General Public License
17
+# along with Bib2011.  If not, see <http://www.gnu.org/licenses/>.
18
+
19
+import os.path
20
+
21
+import reportlab
22
+from reportlab.lib import fonts
23
+
24
+from .InvoiceObjects import InvoiceTable, InvoiceText, InvoiceImage
25
+import re
26
+
27
+# reportlab imports
28
+from reportlab.lib.units import cm, inch
29
+from reportlab.lib.pagesizes import A4
30
+from reportlab.pdfbase.ttfonts import TTFont, TTFError
31
+from reportlab.pdfbase import pdfmetrics
32
+from reportlab.pdfgen import canvas as Canvas
33
+
34
+
35
+def _formatPrice(price, symbol='€'):
36
+    '''_formatPrice(price, symbol='€'):
37
+    Gets a floating point value and returns a formatted price, suffixed by 'symbol'. '''
38
+    s = ("%.2f" % price).replace('.', ',')
39
+    pat = re.compile(r'([0-9])([0-9]{3}[.,])')
40
+    while pat.search(s):
41
+        s = pat.sub(r'\1.\2', s)
42
+    return s+' '+symbol
43
+
44
+def find_font_file(filename):
45
+    for n in range(4):
46
+        candidate = os.path.abspath(os.path.join(os.path.dirname(__file__), '../' * n, 'ressource/fonts', filename))
47
+        if os.path.exists(candidate):
48
+            return candidate
49
+
50
+
51
+def _registerFonts():
52
+    fonts = [
53
+        ("DejaVu", "DejaVuSans.ttf"),
54
+        ("DejaVu-Bold", "DejaVuSans-Bold.ttf"),
55
+        ("DejaVu-Italic", "DejaVuSans-Oblique.ttf"),
56
+        ("DejaVu-BoldItalic", "DejaVuSans-BoldOblique.ttf")
57
+        ]
58
+    for fontname, fontfile in fonts:
59
+        found = False
60
+        try:
61
+            pdfmetrics.registerFont(TTFont(fontname, fontfile))
62
+            found = True
63
+        except TTFError:
64
+            pass
65
+        if not found:
66
+            f = find_font_file(fontfile)
67
+            if f:
68
+                pdfmetrics.registerFont(TTFont(fontname, f))
69
+
70
+
71
+class PDF(object):
72
+    # Set default font size
73
+    default_font_size = 8
74
+    font = 'DejaVu'
75
+    # set margins
76
+    topmargin = 2*cm
77
+    bottommargin = 2.2*cm
78
+    leftmargin = 2*cm
79
+    rightmargin = 2*cm
80
+    rightcolumn = 13*cm
81
+    
82
+    canvas = None
83
+    num_pages = 1
84
+    font_height = 0.3*cm
85
+    line_padding = 0.1*cm
86
+    line_height = font_height+0.1*cm
87
+    
88
+    invoice = None
89
+    
90
+    def __init__(self, invoice):
91
+        _registerFonts()
92
+        from io import BytesIO
93
+        self.fd = BytesIO()
94
+        self.canvas = Canvas.Canvas(self.fd, pagesize=A4)
95
+
96
+        self.invoice = invoice
97
+
98
+        self.topcontent = -self.topmargin
99
+        self.leftcontent = self.leftmargin
100
+        self.rightcontent = A4[0] - self.rightmargin
101
+        self.bottomcontent =  -(A4[1] - self.bottommargin)
102
+
103
+        self.font_size = 8
104
+        self.x = 2.0 * cm
105
+        self.y = -4.8 * cm - self.font_size - 1
106
+        
107
+        self.canvas.setFont(self.font, self.font_size)
108
+    
109
+
110
+    def _splitToWidth(self, text, width, font, size):
111
+        '''_splitToWidth(canvas, text, width, font, size)
112
+        Split a string to several lines of a given width.'''
113
+        lines = []
114
+        paras = text.split('\n')
115
+        for para in paras:
116
+            words = para.split(' ')
117
+            while len(words) > 0:
118
+                mywords = [words[0], ]
119
+                del words[0]
120
+                while len(words) > 0 and self.canvas.stringWidth(' '.join(mywords) + ' ' + words[0], font, size) <= width:
121
+                    mywords.append(words[0])
122
+                    del words[0]
123
+                lines.append(' '.join(mywords))
124
+        return lines
125
+    
126
+    
127
+    def _PageMarkers(self):
128
+        """Setzt Falzmarken"""
129
+        self.canvas.setStrokeColor((0,0,0))
130
+        self.canvas.setLineWidth(0.01*cm)
131
+        self.canvas.lines([(0.3*cm,-10.5*cm,0.65*cm,-10.5*cm),
132
+                           (0.3*cm,-21.0*cm,0.65*cm,-21.0*cm),
133
+                           (0.3*cm,-14.85*cm,0.7*cm,-14.85*cm)])
134
+
135
+
136
+    def _lineHeight(self, fontsize=None, font=None):
137
+        if not fontsize:
138
+            fontsize = self.default_font_size
139
+        if not font:
140
+            font = self.font
141
+        face = pdfmetrics.getFont(font).face
142
+        string_height = (face.ascent - face.descent) / 1000 * fontsize
143
+        return string_height + 0.1*cm
144
+    
145
+    def _partHeight(self, part):
146
+        height = 0
147
+        if type(part) == InvoiceText:
148
+            left, right = self.leftcontent, self.rightcontent
149
+            if part.urgent:
150
+                left += 1.5*cm
151
+                right -= 1.5*cm
152
+                height += len(part.paragraphs) * 3 * self.line_padding
153
+                # Rechne eine Zeile mehr für den Rahmen
154
+                height += self.line_height
155
+            if part.headline:
156
+                height += (len(self._splitToWidth(part.headline, right-left, self.font+'-Bold', self.default_font_size+1)) * self.line_height) + self.line_padding
157
+            for para in part.paragraphs:
158
+                height += (len(self._splitToWidth(para, right-left, self.font, self.default_font_size)) * self.line_height) + self.line_padding
159
+        elif type(part) == InvoiceTable:
160
+            # Eine Zeile plus 2 mal line_padding für Tabellenkopf
161
+            height = self.line_height + 2 * self.line_padding
162
+            # Wenn nur ein Element (plus Summen) hin passt, reicht uns das
163
+            el = part.entries[0]
164
+            # Die Abstände oben und unten
165
+            height += 2 * self.line_padding
166
+            # Die Breite ist konservativ
167
+            if el['type'] == 'title':
168
+                height += self.line_height + 0.2*cm
169
+            else:
170
+                height += self.line_height*len(self._splitToWidth(el['subject'], 9.3*cm, self.font, self.font_size))
171
+            if 'desc' in el and el['desc'] != '':
172
+                height += self.line_height * len(self._splitToWidth(el['desc'], 11*cm, self.font, self.font_size))
173
+            if part.vatType == 'net':
174
+                # Eine Zeile mehr
175
+                height += self.line_height + self.line_padding
176
+            # Für die MwSt-Summen
177
+            height += (self.line_height + self.line_padding) * len(part.vat)
178
+            # Für den Rechnungsbetrag
179
+            height += self.line_height + self.line_padding
180
+        return height
181
+
182
+
183
+    def _tableHead(self, part):
184
+        self.canvas.setFont(self.font, self.font_size)
185
+        self.canvas.drawString(self.leftcontent+(0.1*cm), self.y-self.line_height+self.line_padding, 'Anz.')
186
+        self.canvas.drawString(self.leftcontent+(2.1*cm), self.y-self.line_height+self.line_padding, 'Beschreibung')
187
+        if len(part.vat) == 1:
188
+            self.canvas.drawRightString(self.leftcontent+(14.3*cm), self.y-self.line_height+self.line_padding, 'Einzelpreis')
189
+        else:
190
+            self.canvas.drawRightString(self.leftcontent+(13.3*cm), self.y-self.line_height+self.line_padding, 'Einzelpreis')
191
+        self.canvas.drawRightString(self.leftcontent+(16.8*cm), self.y-self.line_height+self.line_padding, 'Gesamtpreis')
192
+        self.canvas.setLineWidth(0.01*cm)
193
+        self.canvas.line(self.leftcontent, self.y - self.line_height, self.rightcontent, self.y - self.line_height)
194
+        self.y -= self.line_height + 0.02*cm
195
+    
196
+    
197
+    def _PageWrap(self):
198
+        '''Seitenumbruch'''
199
+        self.num_pages += 1
200
+        self.canvas.setFont(self.font, self.default_font_size-2)
201
+        self.canvas.drawRightString(self.rightcontent, self.bottomcontent + self.line_padding, 'Fortsetzung auf Seite %i' % self.num_pages)
202
+        self.canvas.showPage()
203
+        self.basicPage()
204
+        self.y = self.topcontent - self.font_size
205
+        self.canvas.setFillColor((0,0,0))
206
+        self.canvas.setFont(self.font, self.font_size-2)
207
+        self.canvas.drawCentredString(self.leftcontent + (self.rightcontent - self.leftcontent) / 2, self.y, '- Seite %i -' % self.num_pages)
208
+  
209
+    
210
+    def _Footer(self):
211
+        self.canvas.setStrokeColor((0, 0, 0))
212
+        self.canvas.setFillColor((0,0,0))
213
+        self.canvas.line(self.leftcontent, self.bottomcontent, self.rightcontent, self.bottomcontent)
214
+        self.canvas.setFont(self.font, 7)
215
+        lines = list(filter(None, [
216
+            self.invoice.seller['trade_name'],
217
+            self.invoice.seller['name'] if self.invoice.seller['trade_name'] else None,
218
+            self.invoice.seller['website'],
219
+            self.invoice.seller['email'],
220
+        ]))
221
+        c = 0
222
+        for line in lines:
223
+            c += 10
224
+            self.canvas.drawString(self.leftcontent, self.bottomcontent - c, line)
225
+
226
+        if self.invoice.seller_vat_id:
227
+            lines = list(filter(None, [
228
+                "USt-ID: " + self.invoice.seller_vat_id
229
+            ]))
230
+            c = 0
231
+            for line in lines:
232
+                c += 10
233
+                self.canvas.drawString(self.leftcontent + ((self.rightcontent - self.leftcontent) // 3), self.bottomcontent - c, line)
234
+
235
+        if not self.invoice.debit:
236
+            iban = self.invoice.seller_bank_data['iban']
237
+            iban = ' '.join([iban[i:i+4] for i in range(0, len(iban), 4)])
238
+            lines = [
239
+                f"IBAN: {iban}",
240
+                self.invoice.seller_bank_data['bankname'],
241
+                f"BIC: {self.invoice.seller_bank_data['bic']}"
242
+            ]
243
+            c = 0
244
+            for line in lines:
245
+                c += 10
246
+                self.canvas.drawString(self.leftcontent + ((self.rightcontent - self.leftcontent) // 3) * 2, self.bottomcontent - c, line)
247
+
248
+    def basicPage(self):
249
+        # Set marker to top.
250
+        self.canvas.translate(0, A4[1])
251
+
252
+        self._PageMarkers()
253
+        self._Footer()
254
+    
255
+
256
+
257
+    def addressBox(self):
258
+        lines = [
259
+            self.invoice.seller['trade_name'],
260
+            self.invoice.seller['address']['line1'],
261
+            self.invoice.seller['address']['line2'],
262
+            self.invoice.seller['address']['line3'],
263
+            self.invoice.seller['address']['postcode'] + ' ' + self.invoice.seller['address']['city_name'],
264
+        ]
265
+        address = ' · '.join(filter(None, lines))
266
+        self.canvas.drawString(self.x, self.y+0.1*cm, f' {address}')
267
+        self.canvas.line(self.x, self.y, self.x + (8.5 * cm), self.y)
268
+        self.y = self.y - self.font_size - 3
269
+        
270
+        font_size = 11
271
+        x = self.x + 0.5*cm
272
+        self.y -= 0.5*cm
273
+        self.canvas.setFont(self.font, font_size)
274
+        addresslines = filter(None, [
275
+            self.invoice.customer['name'].strip(),
276
+            self.invoice.customer['address']['line1'] or '',
277
+            self.invoice.customer['address']['line2'] or '',
278
+            self.invoice.customer['address']['line3'] or '',
279
+            ((self.invoice.customer['address']['postcode'] or '') + ' ' + (self.invoice.customer['address']['city_name'] or '')).strip(),
280
+        ])
281
+        for line in addresslines:
282
+            self.canvas.drawString(x, self.y, line)
283
+            self.y -= font_size * 0.03527 * cm * 1.2
284
+
285
+
286
+    def firstPage(self):
287
+        self.basicPage()
288
+        self.addressBox()
289
+        
290
+        self.y = self.topcontent
291
+        self.canvas.drawImage(self.invoice.logo_image_file, self.rightcolumn, self.topcontent-(2*cm),
292
+                              height=2*cm, preserveAspectRatio=True, anchor='nw')
293
+        self.y -= (2.5*cm)
294
+        self.canvas.setFont(self.font+"-Bold", self.font_size)
295
+        self.canvas.drawString(self.rightcolumn, self.y, self.invoice.seller['trade_name'] or self.invoice.seller['name'])
296
+        self.y -= (self.font_size + 5)
297
+        self.canvas.setFont(self.font, self.font_size)
298
+        lines = [
299
+            self.invoice.seller['name'] if self.invoice.seller['trade_name'] else None,
300
+            self.invoice.seller['address']['line1'],
301
+            self.invoice.seller['address']['line2'],
302
+            self.invoice.seller['address']['line3'],
303
+            self.invoice.seller['address']['postcode'] + ' ' + self.invoice.seller['address']['city_name'],
304
+            self.invoice.seller['website'],
305
+        ]
306
+        address = filter(None, lines)
307
+        for line in address:
308
+            self.canvas.drawString(self.rightcolumn, self.y, line)
309
+            self.y -= (self.font_size + 5)
310
+        self.y -= 5
311
+        self.canvas.drawString(self.rightcolumn, self.y, f"Tel: {self.invoice.seller['phone']}")
312
+        self.y -= (self.font_size + 5)
313
+        self.canvas.drawString(self.rightcolumn, self.y, f"E-Mail: {self.invoice.seller['email']}")
314
+        self.y -= (self.font_size + 10)
315
+        self.y = -9.5*cm
316
+
317
+
318
+    def title(self, title):
319
+        self.canvas.setTitle(title)
320
+        self.canvas.drawString(self.leftcontent, self.y, title)
321
+
322
+
323
+    def renderRechnung(self):
324
+        self.firstPage()
325
+        self.canvas.setFont(self.font+'-Bold', self.font_size+3)
326
+        self.title(self.invoice.title)
327
+
328
+        if self.invoice.tender:
329
+            self.canvas.setFont(self.font, self.font_size)
330
+            self.canvas.drawString(self.rightcolumn, self.y, "Erstellungsdatum:")
331
+            self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.date.strftime('%d. %m. %Y'))
332
+            self.y -= (self.font_size + 0.1*cm)
333
+        else:
334
+            self.canvas.setFont(self.font+'-Bold', self.font_size)
335
+            self.canvas.drawString(self.rightcolumn, self.y, "Bei Fragen bitte immer angeben:")
336
+            self.y -= (self.font_size + 0.2*cm)
337
+            self.canvas.setFont(self.font, self.font_size)
338
+            self.canvas.drawString(self.rightcolumn, self.y, "Rechnungsdatum:")
339
+            self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.date.strftime('%d. %m. %Y'))
340
+            self.y -= (self.font_size + 0.1*cm)
341
+            self.canvas.drawString(self.rightcolumn, self.y, "Rechnungsnummer:")
342
+            self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.id)
343
+            self.y -= (self.font_size + 0.1*cm)
344
+        if self.invoice.customerno:
345
+            self.canvas.drawString(self.rightcolumn, self.y, "Kundennummer:")
346
+            self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.customerno)
347
+            self.y -= (self.font_size + 0.5*cm)
348
+        self.canvas.setFont(self.font, self.font_size)
349
+        
350
+        if self.invoice.salutation:
351
+            self.canvas.drawString(self.leftcontent, self.y, self.invoice.salutation)
352
+            self.y -= self.font_size + 0.2*cm
353
+            introText = 'hiermit stellen wir Ihnen die nachfolgend genannten Leistungen in Rechnung.'
354
+            if self.invoice.tender:
355
+                introText = 'hiermit unterbreiten wir Ihnen folgendes Angebot.'
356
+            intro = self._splitToWidth(introText, self.rightcontent - self.leftcontent, self.font, self.font_size)
357
+            for line in intro:
358
+                self.canvas.drawString(self.leftcontent, self.y, line)
359
+                self.y -= self.font_size + 0.1*cm
360
+            self.y -= self.font_size + 0.1*cm
361
+        
362
+        
363
+        font_size = self.default_font_size
364
+        for part in self.invoice.parts:
365
+            if self.y - self._partHeight(part) < (self.bottomcontent + (0.5*cm)):
366
+                self._PageWrap()
367
+                self.y = self.topcontent - self.font_size - self.line_padding*3
368
+            if type(part) == InvoiceTable:
369
+                  
370
+                left = self.leftcontent
371
+                right = self.rightcontent
372
+                self._tableHead(part)
373
+                temp_sum = 0.0
374
+                odd = True
375
+                for el in part.entries:
376
+                    if el['type'] == 'title':
377
+                        self.y -= self.line_padding + 0.2*cm
378
+                        self.canvas.setFillColorRGB(0, 0, 0)
379
+                        self.canvas.setFont(self.font+'-Italic', font_size)
380
+                        self.canvas.drawString(left, self.y-self.font_height, el['title'])
381
+                        self.canvas.setFont(self.font, font_size)
382
+                        self.y -= self.line_height + self.line_padding
383
+                    else:
384
+                        subject = []
385
+                        if len(part.vat) == 1:
386
+                            subject = self._splitToWidth(el['subject'], 9.8*cm, self.font, font_size)
387
+                        else:
388
+                            subject = self._splitToWidth(el['subject'], 8.8*cm, self.font, font_size)
389
+                        desc = []
390
+                        if 'desc' in el and el['desc'] != '':
391
+                            desc = self._splitToWidth(el['desc'], 14.0*cm, self.font, font_size)
392
+                        if 'period_start' in el and el['period_start']:
393
+                            if 'period_end' in el and el['period_end']:
394
+                                desc.extend(self._splitToWidth('Leistungszeitraum: %s - %s' %
395
+                                                               (el['period_start'].strftime('%d.%m.%Y'),
396
+                                                                el['period_end'].strftime('%d.%m.%Y')),
397
+                                                               14.0*cm, self.font, font_size))
398
+                            else:
399
+                                desc.extend(self._splitToWidth('Leistungsdatum: %s' %
400
+                                                               (el['period_start'].strftime('%d.%m.%Y'),),
401
+                                                               14.0*cm, self.font, font_size))
402
+                        need_lines = len(subject) + len(desc)
403
+                        # need page wrap?
404
+                        if self.y - (need_lines+1 * (self.line_height + self.line_padding)) < (self.bottomcontent + 1*cm):
405
+                            self.canvas.setFont(self.font + '-Italic', font_size)
406
+                            # Zwischensumme
407
+                            self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Zwischensumme:')
408
+                            self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(temp_sum))
409
+                            # page wrap
410
+                            self._PageWrap()
411
+                            self.y = self.topcontent - font_size - self.line_padding*3
412
+                            # header
413
+                            self._tableHead(part)
414
+                            self.y -= self.line_padding * 3
415
+                            odd=True
416
+                            # übertrag
417
+                            self.canvas.setFont(self.font + '-Italic', font_size)
418
+                            self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Übertrag:')
419
+                            self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(temp_sum))
420
+                            self.y -= self.font_height + self.line_padding * 3
421
+                            self.canvas.setFont(self.font, self.default_font_size)
422
+                            
423
+                        # Zwischensumme (inkl. aktueller Posten)
424
+                        temp_sum += el['total']
425
+    
426
+                        
427
+                        # draw the background
428
+                        if not odd:
429
+                            self.canvas.setFillColorRGB(0.9, 0.9, 0.9)
430
+                        else:
431
+                            self.canvas.setFillColorRGB(1, 1, 1)
432
+                        self.canvas.rect(left, self.y - (need_lines*self.line_height)-(2*self.line_padding), height = (need_lines*self.line_height)+(2*self.line_padding), width = right-left, fill=1, stroke=0)
433
+                        self.canvas.setFillColorRGB(0, 0, 0)
434
+                        self.y -= self.line_padding
435
+                        self.canvas.drawRightString(left+1.1*cm, self.y-self.font_height, '%.0f' % el['count'])
436
+                        if el['unit']:
437
+                            self.canvas.drawString(left+1.2*cm, self.y-self.font_height, el['unit'])
438
+                        self.canvas.drawString(left+2.2*cm, self.y-self.font_height, subject[0])
439
+                        if len(part.vat) == 1:
440
+                            self.canvas.drawRightString(left+14.3*cm, self.y-self.font_height, _formatPrice(el['price']))
441
+                        else:
442
+                            self.canvas.drawRightString(left+13.3*cm, self.y-self.font_height, _formatPrice(el['price']))
443
+                            self.canvas.drawString(left+13.7*cm, self.y-self.font_height, str(part.vat[el['vat']][1]))
444
+                        if el['tender']:  
445
+                            self.canvas.drawRightString(left+16.8*cm, self.y-self.font_height, 'eventual')
446
+                        else:
447
+                            self.canvas.drawRightString(left+16.8*cm, self.y-self.font_height, _formatPrice(el['total']))
448
+                        subject = subject[1:]
449
+                        x = 1
450
+                        for line in subject:
451
+                            self.canvas.drawString(left+2.2*cm, self.y-(x * self.line_height)-self.font_height, line)
452
+                            x += 1
453
+                        for line in desc:
454
+                            self.canvas.drawString(left+2.2*cm, self.y-(x * self.line_height)-self.font_height, line)
455
+                            x += 1
456
+                        odd = not odd
457
+                        self.y -= (need_lines * self.line_height) + self.line_padding
458
+                if part.summary:
459
+                    need_lines = 5
460
+                    if self.y - (need_lines+1 * (self.line_height + self.line_padding)) < (self.bottomcontent + 1*cm):
461
+                        self.canvas.setFont(self.font + '-Italic', font_size)
462
+                        # Zwischensumme
463
+                        self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Zwischensumme:')
464
+                        self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(temp_sum))
465
+                        # page wrap
466
+                        self._PageWrap()
467
+                        self.y = self.topcontent - font_size - self.line_padding*3
468
+                        # header
469
+                        self._tableHead(part)
470
+                        odd=True
471
+                        # übertrag
472
+                        self.canvas.setFont(self.font + '-Italic', font_size)
473
+                        self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Übertrag:')
474
+                        self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(temp_sum))
475
+                        self.y -= self.font_height + self.line_padding
476
+                    self.y -= (0.3*cm)
477
+                    if part.vatType == 'gross':
478
+                        self.canvas.setFont(self.font+'-Bold', font_size)
479
+                        if self.invoice.tender or not self.invoice.official:
480
+                            self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Gesamtbetrag:')
481
+                        else:
482
+                            self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Rechnungsbetrag:')
483
+                        self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(part.sum))
484
+                        if self.invoice.official:
485
+                            self.canvas.setFont(self.font, font_size)
486
+                            self.y -= self.line_height + self.line_padding
487
+                            summaries = []
488
+                            vat = 0.0
489
+                            if len(part.vat) == 1 and list(part.vat.keys())[0] == 0.0:
490
+                                self.canvas.drawString(left, self.y - self.font_height,
491
+                                                       'Dieser Beleg wurde ohne Ausweis von MwSt erstellt.')
492
+                                self.y -= self.line_height
493
+                            else:
494
+                                if len(part.vat) == 1:
495
+                                    vat = list(part.vat.keys())[0]
496
+                                    if self.invoice.tender:
497
+                                        summaries.append(('Im Gesamtbetrag sind %.1f%% MwSt enthalten:' % (vat*100), _formatPrice((part.sum/(vat+1))*vat)))
498
+                                    else:
499
+                                        summaries.append(('Im Rechnungsbetrag sind %.1f%% MwSt enthalten:' % (vat*100), _formatPrice((part.sum/(vat+1))*vat)))
500
+                                else:
501
+                                    for vat, vatdata in part.vat.items():
502
+                                        if vat > 0:
503
+                                            summaries.append(('%s: Im Teilbetrag von %s sind %.1f%% MwSt enthalten:' % (vatdata[1], _formatPrice(vatdata[0]), vat*100), _formatPrice((vatdata[0]/(vat+1))*vat)))
504
+                                        else:
505
+                                            summaries.append(('%s: Durchlaufende Posten ohne Berechnung von MwSt.' % (vatdata[1]), 0.0))
506
+                            summaries.append(('Nettobetrag:', _formatPrice(part.sum - (part.sum/(vat+1))*vat)))
507
+                            summaries.sort()
508
+                            for line in summaries:
509
+                                self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, line[0])
510
+                                if line[1]:
511
+                                    self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, line[1])
512
+                                self.y -= self.line_height
513
+                    elif len(part.vat) == 1 and part.vatType == 'net':
514
+                        self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Nettobetrag:')
515
+                        self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(part.sum))
516
+                        self.y -= self.line_height
517
+                        summaries = []
518
+                        if list(part.vat.keys())[0] == 0.0:
519
+                            self.canvas.drawString(left, self.y-self.font_height, 'Dieser Beleg wurde ohne Ausweis von MwSt erstellt.')
520
+                            self.y -= self.line_height
521
+                        else:
522
+                            if len(part.vat) == 1:
523
+                                vat = list(part.vat.keys())[0]
524
+                                summaries.append(('zzgl. %.1f%% MwSt:' % (vat*100), _formatPrice(vat*part.sum)))
525
+                            else:
526
+                                for vat, vatdata in part.vat.items():
527
+                                    summaries.append(('zzgl. %.1f%% MwSt (%s):' % (vat*100, vatdata[1]), _formatPrice(vat*vatdata[0])))
528
+                        summaries.sort()
529
+                        for line in summaries:
530
+                            self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, line[0])
531
+                            self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, line[1])
532
+                            self.y -= self.line_height
533
+                        sum = 0
534
+                        for vat, vatdata in part.vat.items():
535
+                            sum += (vat+1)*vatdata[0]
536
+                        self.canvas.setFont(self.font+'-Bold', font_size)
537
+                        if self.invoice.tender:
538
+                            self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Gesamtbetrag:')
539
+                        else:
540
+                            self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Rechnungsbetrag:')
541
+                        self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(sum))
542
+                        self.canvas.setFont(self.font, font_size)
543
+                        self.y -= self.line_height + self.line_padding
544
+                    paysum = 0.0
545
+                    for pay in part.payments:
546
+                        paysum += pay['amount']
547
+                        descr = 'Zahlung' 
548
+                        if pay['type'] == 'cash':
549
+                            descr = 'gegeben'
550
+                        elif pay['type'] == 'return':
551
+                            descr = 'zurück'
552
+                        elif pay['type'] == 'ec':
553
+                            descr = 'Kartenzahlung (EC)'
554
+                        elif pay['type'] == 'gutschein':
555
+                            descr = 'Einlösung Gutschein'
556
+                        if pay['date'] != self.invoice.date:
557
+                            descr += ' am %s' % (pay['date'].strftime('%d. %m. %Y'))
558
+                        self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, descr + ':')
559
+                        self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(pay['amount']))
560
+                        self.y -= self.line_height
561
+                    sum = part.sum
562
+                    if part.vatType == 'net':
563
+                        sum = 0
564
+                        for vat, vatdata in part.vat.items():
565
+                            sum += (vat+1)*vatdata[0] 
566
+                    rest = sum - paysum
567
+                    if part.payments and rest > 0:
568
+                        self.canvas.setFont(self.font+'-Bold', font_size)
569
+                        self.canvas.drawRightString(left + 14.5*cm, self.y-self.font_height, 'Offener Rechnungsbetrag:')
570
+                        self.canvas.drawRightString(left + 16.8*cm, self.y-self.font_height, _formatPrice(rest))
571
+                        self.canvas.setFont(self.font, font_size)
572
+                        self.y -= self.line_height + self.line_padding
573
+                        
574
+                    self.y -= self.line_padding
575
+            elif type(part) == InvoiceText:
576
+                my_font_size = font_size + part.fontsize # Relative Schriftgröße beachten
577
+                self.canvas.setFont(self.font, my_font_size)
578
+                left, right = self.leftcontent, self.rightcontent
579
+                firsttime = True
580
+                headlines = []
581
+                if part.urgent:
582
+                    left += 1.5*cm
583
+                    right -= 1.5*cm
584
+                if part.headline:
585
+                    headlines = self._splitToWidth(part.headline, right-left, self.font, my_font_size)
586
+                for para in part.paragraphs:
587
+                    lines = self._splitToWidth(para, right-left, self.font, my_font_size)
588
+                    if part.urgent:
589
+                        need_height = len(lines) * self._lineHeight(my_font_size)
590
+                        if len(headlines) > 0:
591
+                            need_height += len(headlines) * (self._lineHeight(my_font_size) + 1) + self.line_padding
592
+                        self.canvas.setFillColorRGB(0.95, 0.95, 0.95)
593
+                        self.canvas.rect(left-0.5*cm, self.y - (need_height+(6*self.line_padding)), height = need_height+(6*self.line_padding), width = right-left+1*cm, fill=1, stroke=1)
594
+                        self.canvas.setFillColorRGB(0, 0, 0)
595
+                        self.y -= self.line_padding*3
596
+                    if part.headline and firsttime:
597
+                        firsttime = False
598
+                        self.canvas.setFont(self.font+'-Bold', my_font_size+1)
599
+                        for line in headlines:
600
+                            self.canvas.drawString(left, self.y-(self.font_height+1), line)
601
+                            self.y -= self._lineHeight(my_font_size) + 1
602
+                        self.y -= self.line_padding
603
+                        self.canvas.setFont(self.font, my_font_size)
604
+                    for line in lines:
605
+                        self.canvas.drawString(left, self.y-self.font_height, line)
606
+                        self.y -= self._lineHeight(my_font_size)
607
+                    self.y -= self.line_padding*3
608
+                left, right = self.leftcontent, self.rightcontent
609
+            elif type(part) == InvoiceImage:
610
+                width = (part.imagedata.width / part.dpi) * inch
611
+                height = width * (part.imagedata.height / part.imagedata.width)
612
+                x = self.leftcontent
613
+                if part.alignment == "center":
614
+                    x = self.leftcontent + (self.rightcontent - self.leftcontent)/2 - width/2
615
+                elif part.alignment == "right":
616
+                    x = self.rightcontent - width
617
+                self.canvas.drawInlineImage(part.imagedata, x, self.y-height, width=width, height=height)
618
+                self.y -= self.line_padding + height
619
+                if part.caption:
620
+                    self.canvas.drawString(x, self.y-self.font_height, part.caption)
621
+                    self.y -= self._lineHeight()
622
+            else:
623
+                raise NotImplementedError("Cannot handle part of type %s" % type(part))
624
+            self.y -= (0.5*cm)
625
+          
626
+        self.canvas.showPage()
627
+        self.canvas.save()
628
+        pdfdata = self.fd.getvalue()
629
+        return pdfdata
630
+
631
+
632
+def InvoiceToPDF(iv):
633
+    pdf = PDF(iv)
634
+    return pdf.renderRechnung()
635
+
636
+
637
+
638
+
639
+if __name__ == '__main__':
640
+    import datetime
641
+    from lib.Speicher import Speicher
642
+    from lib.BelegRechnung import BelegRechnung
643
+    s = Speicher()
644
+    import sys, os
645
+    renr = 'R2024-3149'
646
+    if len(sys.argv) > 1:
647
+        renr=sys.argv[1]
648
+    kb = s.getKassenbeleg(renr=renr)
649
+
650
+    filename = BelegRechnung(kb)
651
+    print (filename)
652
+
653
+
654
+
... ...
@@ -0,0 +1,253 @@
1
+# -*- coding: utf-8 -*-
2
+# (C) 2011 by Bernd Wurst <bernd@schokokeks.org>
3
+
4
+# This file is part of Bib2011.
5
+#
6
+# Bib2011 is free software: you can redistribute it and/or modify
7
+# it under the terms of the GNU General Public License as published by
8
+# the Free Software Foundation, either version 3 of the License, or
9
+# (at your option) any later version.
10
+#
11
+# Bib2011 is distributed in the hope that it will be useful,
12
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
+# GNU General Public License for more details.
15
+#
16
+# You should have received a copy of the GNU General Public License
17
+# along with Bib2011.  If not, see <http://www.gnu.org/licenses/>.
18
+
19
+import os.path, sys
20
+from decimal import Decimal
21
+import datetime
22
+
23
+import re
24
+
25
+# Search for included submodule python-drafthorse
26
+atoms = os.path.abspath(os.path.dirname(__file__)).split('/')
27
+dir = ''
28
+while atoms:
29
+    candidate = os.path.join('/'.join(atoms), 'external/python-drafthorse')
30
+    if os.path.exists(candidate):
31
+        dir = candidate
32
+        break
33
+    atoms = atoms[:-1]
34
+sys.path.insert(0, dir)
35
+from drafthorse.models.document import Document
36
+from drafthorse.models.accounting import ApplicableTradeTax
37
+from drafthorse.models.tradelines import LineItem
38
+from .InvoiceObjects import InvoiceTable, InvoiceText, InvoiceImage, RECHNUNG, GUTSCHRIFT, KORREKTUR, \
39
+    VAT_REGULAR, VAT_KLEINUNTERNEHMER, VAT_INNERGEM
40
+from drafthorse.models.party import TaxRegistration, URIUniversalCommunication
41
+from drafthorse.models.payment import PaymentTerms
42
+from drafthorse.models.note import IncludedNote
43
+from drafthorse.pdf import attach_xml
44
+
45
+def InvoiceToXML(invoice):
46
+    doc = Document()
47
+    doc.context.guideline_parameter.id = "urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended"
48
+    doc.header.id = invoice.id
49
+    # Typecodes:
50
+    # 380: Handelsrechnungen
51
+    # 381: Gutschrift
52
+    # 384: Korrekturrechnung
53
+    # 389: Eigenrechnung (vom Käufer im Namen des Lieferanten erstellt).
54
+    # 261: Selbstverfasste Gutschrift.
55
+    # 386: Vorauszahlungsrechnung
56
+    # 326: Teilrechnung
57
+    # 751: Rechnungsinformation - KEINE RECHNUNG
58
+    if invoice.type == RECHNUNG:
59
+        doc.header.type_code = "380"
60
+    elif invoice.type == GUTSCHRIFT:
61
+        doc.header.type_code = "381"
62
+    elif invoice.type == KORREKTUR:
63
+        doc.header.type_code = "384"
64
+    else:
65
+        raise TypeError("Unbekannter Rechnungstyp, kann kein XML erstellen")
66
+    doc.header.issue_date_time = invoice.date
67
+
68
+    # Seller-Address
69
+    if invoice.seller['trade_name']:
70
+        pass
71
+        # FIXME: specified_legal_organization ist in der Library nicht implementiert, pull request ist vorhanden
72
+        #doc.trade.agreement.seller.specified_legal_organization.trade_name = invoice.seller['trade_name']
73
+    doc.trade.agreement.seller.name = invoice.seller['name']
74
+    doc.trade.agreement.seller.address.country_id = invoice.seller['address']['country_id']
75
+    doc.trade.agreement.seller.address.postcode = invoice.seller['address']['postcode']
76
+    doc.trade.agreement.seller.address.city_name = invoice.seller['address']['city_name']
77
+    doc.trade.agreement.seller.address.line_one = invoice.seller['address']['line1']
78
+    doc.trade.agreement.seller.address.line_two = invoice.seller['address']['line2']
79
+    doc.trade.agreement.seller.address.line_three = invoice.seller['address']['line3']
80
+    if invoice.seller_vat_id:
81
+        tax_reg = TaxRegistration()
82
+        tax_reg.id = ('VA', invoice.seller_vat_id)
83
+        doc.trade.agreement.seller.tax_registrations.add(tax_reg)
84
+    if invoice.seller['email']:
85
+        email = URIUniversalCommunication()
86
+        email.uri_ID = ('EM', invoice.seller['email'])
87
+        # FIXME: Typo in der Library ("adress")?
88
+        doc.trade.agreement.seller.electronic_adress.add(email)
89
+
90
+    # Buyer-Address
91
+    doc.trade.agreement.buyer.name = invoice.customer['name']
92
+    doc.trade.agreement.buyer.address.country_id = invoice.customer['address']['country_id']
93
+    doc.trade.agreement.buyer.address.postcode = invoice.customer['address']['postcode']
94
+    doc.trade.agreement.buyer.address.city_name = invoice.customer['address']['city_name']
95
+    doc.trade.agreement.buyer.address.line_one = invoice.customer['address']['line1']
96
+    doc.trade.agreement.buyer.address.line_two = invoice.customer['address']['line2']
97
+    doc.trade.agreement.buyer.address.line_three = invoice.customer['address']['line3']
98
+
99
+    # Line Items
100
+    summe_netto = 0.0
101
+    summe_brutto = 0.0
102
+    summe_bezahlt = 0.0
103
+    summe_ust = 0.0
104
+    line_id_count = 0
105
+    textparts = []
106
+    for part in invoice.parts:
107
+        if type(part) == InvoiceText:
108
+            textparts += part.paragraphs
109
+        if type(part) == InvoiceTable:
110
+            for el in part.entries:
111
+                line_id_count += 1
112
+                li = LineItem()
113
+                li.document.line_id = f"{line_id_count}"
114
+                li.product.name = el['subject']
115
+                if 'desc' in el and el['desc'] != '':
116
+                    desc = li.product.description = el['desc']
117
+
118
+                if 'period_start' in el and el['period_start']:
119
+                    if 'period_end' in el and el['period_end']:
120
+                        li.settlement.period.start = el['period_start']
121
+                        li.settlement.period.end = el['period_end']
122
+                    else:
123
+                        li.delivery.event.occurrence = el['period_start']
124
+
125
+                # FIXME: Hier sollte der passende Code benutzt werden (z.B. Monat)
126
+                li.delivery.billed_quantity = (Decimal(el['count']), 'H87')
127
+                # LTR = Liter (1 dm3)
128
+                # MTQ = cubic meter
129
+                # KGM = Kilogram
130
+                # MTR = Meter
131
+                # H87 = Piece
132
+                # TNE = Tonne
133
+                # MON = Month
134
+
135
+                li.settlement.trade_tax.type_code = "VAT"
136
+                if invoice.vat_type == VAT_REGULAR:
137
+                    li.settlement.trade_tax.category_code = "S"
138
+                elif invoice.vat_type == VAT_KLEINUNTERNEHMER:
139
+                    li.settlement.trade_tax.category_code = "E"
140
+                elif invoice.vat_type == VAT_INNERGEM:
141
+                    li.settlement.trade_tax.category_code = "K"
142
+                # FIXME: Typ bei uns nur global gesetzt, nicht pro Artikel
143
+                # S = Standard VAT rate
144
+                # Z = Zero rated goods
145
+                # E = VAT exempt
146
+                # AE = Reverse charge
147
+                # K = Intra-Community supply (specific reverse charge)
148
+                # G = Exempt VAT for Export outside EU
149
+                # O = Outside VAT scope
150
+                li.settlement.trade_tax.rate_applicable_percent = Decimal(f"{el['vat']*100:.1f}")
151
+
152
+                nettopreis = el['price']
153
+                if part.vatType == 'gross':
154
+                    nettopreis = el['price'] / (1 + el['vat'])
155
+                li.agreement.net.amount = Decimal(f"{nettopreis:.2f}")
156
+
157
+                nettosumme = el['total']
158
+                if part.vatType == 'gross':
159
+                    nettosumme = el['total'] / (1 + el['vat'])
160
+                li.settlement.monetary_summation.total_amount = Decimal(f"{nettosumme:.2f}")
161
+
162
+                summe_netto += nettosumme
163
+                summe_brutto += el['total']
164
+                doc.trade.items.add(li)
165
+
166
+            for pay in part.payments:
167
+                summe_bezahlt += pay['amount']
168
+
169
+            for vat, vatdata in part.vat.items():
170
+                trade_tax = ApplicableTradeTax()
171
+                # Steuerbetrag dieses Steuersatzes
172
+                trade_tax.calculated_amount = Decimal(f"{(vatdata[0] / (vat + 1)) * vat:.2f}")
173
+                # Nettosumme dieses Steuersatzes
174
+                trade_tax.basis_amount = Decimal(f"{(vatdata[0] / (vat + 1)):.2f}")
175
+                trade_tax.type_code = "VAT"
176
+                if invoice.vat_type == VAT_REGULAR:
177
+                    trade_tax.category_code = "S"
178
+                elif invoice.vat_type == VAT_KLEINUNTERNEHMER:
179
+                    trade_tax.category_code = "E"
180
+                    trade_tax.exemption_reason = 'Als Kleinunternehmer wird gemäß §19 UStG keine USt in Rechnung gestellt.'
181
+                elif invoice.vat_type == VAT_INNERGEM:
182
+                    trade_tax.category_code = "K"
183
+                trade_tax.rate_applicable_percent = Decimal(f"{vat*100:.1f}")
184
+                summe_ust += (vatdata[0] / (vat + 1)) * vat
185
+                doc.trade.settlement.trade_tax.add(trade_tax)
186
+
187
+    for paragraph in textparts:
188
+        note = IncludedNote()
189
+        note.content.add(paragraph)
190
+        doc.header.notes.add(note)
191
+
192
+    rest = summe_brutto - summe_bezahlt
193
+
194
+    if invoice.creditor_reference_id:
195
+        # Gläubiger-ID für SEPA
196
+        doc.trade.settlement.creditor_reference_id = invoice.creditor_reference_id
197
+    doc.trade.settlement.payment_reference = invoice.id
198
+    doc.trade.settlement.currency_code = 'EUR'
199
+    if invoice.debit:
200
+        doc.trade.settlement.payment_means.type_code = "59"
201
+    else:
202
+        doc.trade.settlement.payment_means.type_code = "30"
203
+    if invoice.seller_bank_data['iban']:
204
+        doc.trade.settlement.payment_means.payee_account.iban = invoice.seller_bank_data['iban']
205
+        doc.trade.settlement.payment_means.payee_institution.bic = "GENODES1VBK"
206
+        # Ist in der Library vorhanden, validiert aber nicht im XML?!
207
+    if invoice.buyer_bank_data['iban']:
208
+        # Kunden-Bankverbindung bei Lastschrift
209
+        doc.trade.settlement.payment_means.payer_account.iban = invoice.buyer_bank_data['iban']
210
+
211
+    terms = PaymentTerms()
212
+    if invoice.due_date:
213
+        terms.description = f"Bitte begleichen Sie den Betrag bis zum {invoice.due_date.strftime('%d.%m.%Y')} ohne Abzüge."
214
+        terms.due = invoice.due_date
215
+    if invoice.debit:
216
+        if invoice.debit_mandate_id:
217
+            # Mandatsreferenz für Lastschrift
218
+            terms.debit_mandate_id = invoice.debit_mandate_id
219
+        terms.description = 'Wir buchen von Ihrem Konto ab.'
220
+    doc.trade.settlement.terms.add(terms)
221
+
222
+
223
+    doc.trade.settlement.monetary_summation.line_total = Decimal(f"{summe_netto:.2f}")
224
+    doc.trade.settlement.monetary_summation.charge_total = Decimal("0.00")
225
+    doc.trade.settlement.monetary_summation.allowance_total = Decimal("0.00")
226
+    doc.trade.settlement.monetary_summation.tax_basis_total = Decimal(f"{summe_netto:.2f}")
227
+    doc.trade.settlement.monetary_summation.tax_total = (Decimal(f"{summe_ust:.2f}"), "EUR")
228
+    doc.trade.settlement.monetary_summation.prepaid_total = Decimal(f"{summe_bezahlt:.2f}")
229
+    doc.trade.settlement.monetary_summation.grand_total = Decimal(f"{summe_brutto:.2f}")
230
+    doc.trade.settlement.monetary_summation.due_amount = Decimal(f"{rest:.2f}")
231
+
232
+
233
+    # Generate XML file
234
+    xml = doc.serialize(schema="FACTUR-X_EXTENDED")
235
+    return xml
236
+
237
+
238
+if __name__ == '__main__':
239
+    import datetime
240
+    from lib.Speicher import Speicher
241
+    from lib.BelegRechnung import BelegRechnung
242
+    s = Speicher()
243
+    import sys, os
244
+    renr = 'R2024-3149'
245
+    if len(sys.argv) > 1:
246
+        renr=sys.argv[1]
247
+    kb = s.getKassenbeleg(renr=renr)
248
+
249
+    filename = BelegRechnung(kb)
250
+    print(filename)
251
+
252
+
253
+
... ...
@@ -1,96 +0,0 @@
1
-# -* coding: utf8 *-
2
-
3
-import datetime
4
-
5
-
6
-class Image:
7
-    def __init__(self, pilimage, caption=None, dpi=80, alignment="left"):
8
-        self.imagedata = pilimage
9
-        self.alignment = alignment
10
-        self.dpi = dpi
11
-        self.caption = caption
12
-
13
-
14
-class Text:
15
-    def __init__(self, content, urgent=False, headline=None):
16
-        self.paragraphs = [content]
17
-        self.urgent = urgent
18
-        self.headline = headline
19
-
20
-    def addParagraph(self, content):
21
-        self.paragraphs.append(content)
22
-
23
-
24
-class Table:
25
-    def __init__(self, vatType='gross', tender=False, summary=True):
26
-        self.entries = []
27
-        self.vat = {}
28
-        self.sum = 0.0
29
-        self.tender = tender
30
-        self.summary = summary
31
-        if vatType not in ['gross', 'net']:
32
-            raise ValueError('vatType must be »gross« or »net«')
33
-        self.vatType = vatType
34
-
35
-    def validEntry(self, entry):
36
-        '''bekommt einen Eintrag und liefert einen Eintrag wenn ok, wirft ansonsten ValueError.
37
-        wird benutzt um z.B. die Summe auszurechnen oder ähnliches
38
-        '''
39
-        e = entry
40
-        if not ('count' in e and 'subject' in e and 'price' in e and 'vat' in e):
41
-            raise ValueError('Some data is missing!')
42
-        if 'unit' not in e:
43
-            e['unit'] = None
44
-        ret = {'count': e['count'],
45
-               'unit': e['unit'],
46
-               'subject': e['subject'],
47
-               'price': e['price'],
48
-               'total': (e['price'] * e['count']),
49
-               'vat': e['vat'],
50
-               'tender': False,
51
-               }
52
-        if 'tender' in e:
53
-            ret['tender'] = e['tender']
54
-        if 'desc' in e:
55
-            ret['desc'] = e['desc']
56
-        return ret
57
-
58
-    def addItem(self, data):
59
-        '''Fügt eine Zeile ein. data muss ein Dict mit passenden Keys und passenden
60
-        Typen sein'''
61
-        d = self.validEntry(data)
62
-        if not d['vat'] in self.vat:
63
-            self.vat[d['vat']] = [0, chr(65 + len(self.vat))]
64
-        if 'tender' not in data or not data['tender']:
65
-            self.vat[d['vat']][0] += d['total']
66
-            self.sum += d['total']
67
-        self.entries.append(d)
68
-
69
-
70
-class Invoice:
71
-    tender = False
72
-    caption = 'Rechnung'
73
-
74
-    def __init__(self):
75
-        self.customerno = None
76
-        self.addresslines = ['', ]
77
-        self.salutation = 'Sehr geehrte Damen und Herren,'
78
-        self.id = None
79
-        self.parts = []
80
-        self.pagecount = 0
81
-        self.date = datetime.date.today()
82
-
83
-    def setDate(self, date):
84
-        if type(date) != datetime.date:
85
-            raise ValueError('date must be of type »datetime.date«')
86
-        self.date = date
87
-
88
-
89
-class Tender(Invoice):
90
-    tender = True
91
-    caption = 'Angebot'
92
-
93
-
94
-class Generic(Invoice):
95
-    tender = False
96
-    caption = ''
97 0