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 |