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 |