git.schokokeks.org
Repositories
Help
Report an Issue
invoice.git
Code
Commits
Branches
Tags
Suche
Strukturansicht:
25674c2
Branches
Tags
factur-x_localdata
master
invoice.git
Invoice
pdf.py
separated from keks-tools
Bernd Wurst
commited
25674c2
at 2024-02-14 08:20:12
pdf.py
Blame
History
Raw
# -* coding: utf8 *- from __future__ import division import Invoice import re # our page size and margins from .metrics import * # our custom page style from .custom_elements import basicPage, firstPage, address_header # reportlab imports from reportlab.lib.units import cm, inch from reportlab.pdfgen import canvas as Canvas from reportlab.lib.colors import Color def _formatPrice(price, symbol='€'): '''_formatPrice(price, symbol='€'): Gets a floating point value and returns a formatted price, suffixed by 'symbol'. ''' s = ("%.2f" % price).replace('.', ',') pat = re.compile(r'([0-9])([0-9]{3}[.,])') while pat.search(s): s = pat.sub(r'\1.\2', s) return s + ' ' + symbol def _niceCount(value): '''_niceCount(value): Returns a tuple (integer , decimals) where decimals can be None''' if type(value) == int: return ('%i' % value, None) if round(value, 2) == int(value): return ('%i' % int(value), None) s = '%.2f' % value (integer, decimals) = s.split('.', 1) if decimals[-1] == '0': decimals = decimals[:-1] return (integer, decimals) def _splitToWidth(canvas, text, width, font, size): '''_splitToWidth(canvas, text, width, font, size) Split a string to several lines of a given width.''' lines = [] paras = text.split('\n') for para in paras: words = para.split(' ') while len(words) > 0: mywords = [words[0], ] del words[0] while len(words) > 0 and canvas.stringWidth(' '.join(mywords) + ' ' + words[0], font, size) <= width: mywords.append(words[0]) del words[0] lines.append(' '.join(mywords)) return lines def _drawJustifiedString(x, y, text, canvas, width, font, size): text = text.strip() if canvas.stringWidth(text, font, size) > width: canvas.drawString(x, y, text) # too long line, I cannot handle this return if ' ' not in text: canvas.drawString(x, y, text) # no space in there, nothing to justify return words = ['%s' % w for w in text.split(' ')] words_width = 0.0 for word in words: words_width += canvas.stringWidth(word, font, size) available_space = width - words_width available_each = available_space // float((len(words) - 1)) my_x = x for idx in range(len(words)): word = words[idx] canvas.drawString(my_x, y, word) my_x += canvas.stringWidth(word, font, size) + available_each return def address(canvas, lines): x = 2.0 * cm y = page_height - 5.0 * cm canvas.setFont(font, 8) canvas.drawString(x + 0.5 * cm, y + 0.1 * cm, 'schokokeks.org · Köchersberg 32 · 71540 Murrhardt') canvas.setLineWidth(1) canvas.line(x + 0.4 * cm, y, x + address_width, y) y = y - 0.2 * cm line_height = 11 + 0.1 * cm y -= line_height fontsize = 11 for line in lines: if canvas.stringWidth(line, font, fontsize) > address_width: # Wenn es in zwei Zeilen passt, dann ist alles okay, ansonsten verkleinern if len(lines) > 4 or canvas.stringWidth(line, font, fontsize) > 2 * address_width: for candidate in [10.5, 10, 9.5, 9, 8.5, 8]: fontsize = candidate if (len(lines) <= 4 and canvas.stringWidth(line, font, fontsize) <= 2 * address_width) or canvas.stringWidth(line, font, fontsize) <= address_width: break for line in lines: if canvas.stringWidth(line, font, fontsize) > address_width: mylines = _splitToWidth(canvas, line, address_width, font, fontsize) for l in mylines: canvas.setFont(font, fontsize) canvas.drawString(x + 0.5 * cm, y, l) y -= line_height else: canvas.setFont(font, fontsize) canvas.drawString(x + 0.5 * cm, y, line) y -= line_height def InvoiceToPDF(iv, bankdata=True): from io import BytesIO fd = BytesIO() canvas = Canvas.Canvas(fd, pagesize=A4) if iv.tender: canvas.setTitle("Angebot von schokokeks.org") else: canvas.setTitle("Rechnung von schokokeks.org") canvas.setFont(font, 12) num_pages = 1 # Waehrungssysmbol symbol = '€' y = topcontent font_size = default_font_size font_height = 0.35 * cm line_padding = 0.1 * cm line_height = font_height + 0.1 * cm def _partHeight(part): height = 0 if type(part) == Invoice.Text: left, right = leftcontent, rightcontent if part.urgent: left += 1.5 * cm right -= 1.5 * cm height += len(part.paragraphs) * 3 * line_padding # Rechne eine Zeile mehr für den Rahmen height += line_height if part.headline: height += (len(_splitToWidth(canvas, part.headline, right - left, font + '-Bold', default_font_size + 1)) * line_height) + line_padding for para in part.paragraphs: height += (len(_splitToWidth(canvas, para, right - left, font, default_font_size)) * line_height) + line_padding elif type(part) == Invoice.Table: # Eine Zeile plus 2 mal line_padding für Tabellenkopf height = line_height + 2 * line_padding # Wenn nur ein Element (plus Summen) hin passt, reicht uns das el = part.entries[0] # Die Abstände oben und unten height += 2 * line_padding # Die Breite ist konservativ height += line_height * len(_splitToWidth(canvas, el['subject'], 9.3 * cm, font, font_size)) if 'desc' in el and el['desc'] != '': height += line_height * len(_splitToWidth(canvas, el['desc'], 11 * cm, font, font_size)) if part.vatType == 'net': # Eine Zeile mehr height += line_height + line_padding # Für die MwSt-Summen height += (line_height + line_padding) * len(part.vat) # Für den Rechnungsbetrag height += line_height + line_padding return height def _tableHead(y): canvas.setFont(font, font_size) canvas.drawString(left + (0.1 * cm), y - line_height + line_padding, 'Anz.') canvas.drawString(left + (2.6 * cm), y - line_height + line_padding, 'Beschreibung') if len(part.vat) == 1: canvas.drawRightString(left + (14.3 * cm), y - line_height + line_padding, 'Einzelpreis') else: canvas.drawRightString(left + (13.7 * cm), y - line_height + line_padding, 'Einzelpreis') canvas.drawRightString(left + (16.8 * cm), y - line_height + line_padding, 'Gesamtpreis') canvas.setLineWidth(0.01 * cm) canvas.line(left, y - line_height, right, y - line_height) y -= line_height + 0.02 * cm return y def _PageWrap(canvas): '''Seitenumbruch''' nonlocal num_pages num_pages += 1 canvas.setFont(font, default_font_size - 2) canvas.drawRightString(rightcontent, bottomcontent + line_padding, 'Fortsetzung auf Seite %i' % num_pages) canvas.showPage() basicPage(canvas) y = topcontent - font_size canvas.setFillColor((0, 0, 0)) canvas.setFont(font, font_size - 2) canvas.drawCentredString(leftcontent + (rightcontent - leftcontent) // 2, y, '- Seite %i -' % num_pages) address(canvas, iv.addresslines) font_size = default_font_size y = firstPage(canvas) if not bankdata: # Bankdaten überschreiben wenn Lastschrift canvas.setFillColor(Color(255, 255, 255, alpha=0.8)) canvas.rect(leftcontent + ((rightcontent - leftcontent) // 3) * 2 - 2, bottomcontent - 2, (rightcontent - leftcontent) // 3, -40, fill=True, stroke=False) canvas.setFillColor(Color(0, 0, 0, alpha=1)) canvas.saveState() canvas.translate(leftcontent + ((rightcontent - leftcontent) // 3) * 2 + 2, bottomcontent - 40) canvas.rotate(15) canvas.drawString(0, 0, "Bitte nicht überweisen") canvas.restoreState() # canvas.drawString(leftcontent+((rightcontent-leftcontent)/3)*2 + 2, bottomcontent - 20, "Bitte nicht überweisen") canvas.setFont(font + '-Bold', font_size + 3) min_y = y if iv.caption: canvas.drawString(leftcontent, y, iv.caption) min_y -= (font_size + 3) + 0.5 * cm if type(iv) == Invoice.Tender: canvas.setFont(font, font_size) canvas.drawString(rightcolumn, y, "Erstellungsdatum:") canvas.drawRightString(rightcontent, y, "%s" % iv.date.strftime('%d. %m. %Y')) y -= (font_size + 0.1 * cm) elif type(iv) == Invoice.Generic: canvas.setFont(font, font_size) canvas.drawString(rightcolumn, y, "Datum:") canvas.drawRightString(rightcontent, y, "%s" % iv.date.strftime('%d. %m. %Y')) y -= (font_size + 0.1 * cm) elif type(iv) == Invoice.Invoice: canvas.setFont(font + '-Bold', font_size) canvas.drawString(rightcolumn, y, "Bei Fragen bitte immer angeben:") y -= (font_size + 0.2 * cm) canvas.setFont(font, font_size) canvas.drawString(rightcolumn, y, "Rechnungsdatum:") canvas.drawRightString(rightcontent, y, "%s" % iv.date.strftime('%d. %m. %Y')) y -= (font_size + 0.1 * cm) canvas.drawString(rightcolumn, y, "Rechnungsnummer:") canvas.drawRightString(rightcontent, y, "%i" % iv.id) y -= (font_size + 0.1 * cm) if iv.customerno: canvas.drawString(rightcolumn, y, "Kundennummer:") canvas.drawRightString(rightcontent, y, "%s" % iv.customerno) y -= (font_size + 0.5 * cm) canvas.setFont(font, font_size) y = min(min_y, y) if iv.salutation: canvas.drawString(leftcontent, y, iv.salutation) y -= font_size + 0.2 * cm if type(iv) in [Invoice.Tender, Invoice.Invoice]: introText = 'hiermit stellen wir Ihnen die nachfolgend genannten Leistungen in Rechnung.' if type(iv) == Invoice.Tender: introText = 'hiermit unterbreiten wir Ihnen folgendes Angebot.' intro = _splitToWidth(canvas, introText, rightcontent - leftcontent, font, font_size) for line in intro: canvas.drawString(leftcontent, y, line) y -= font_size + 0.1 * cm y -= font_size + 0.1 * cm font_size = default_font_size for part in iv.parts: if y - _partHeight(part) < (bottomcontent + (0.5 * cm)): _PageWrap(canvas) y = topcontent - font_size - line_padding * 3 # Debug: Was hat die Höhenbestimmung für diesen Teil als Höhe herausgefunden? # canvas.line(leftcontent, y-_partHeight(part), rightcontent, y-_partHeight(part)) if type(part) == Invoice.Table: left = leftcontent right = rightcontent top = topcontent bottom = bottomcontent temp_sum = 0.0 y = _tableHead(y) odd = True for el in part.entries: subject = [] if len(part.vat) == 1: subject = _splitToWidth(canvas, el['subject'], 10.3 * cm, font, font_size) else: subject = _splitToWidth(canvas, el['subject'], 9.3 * cm, font, font_size) desc = [] if 'desc' in el and el['desc'] != '': desc = _splitToWidth(canvas, el['desc'], 11 * cm, font, font_size) need_lines = len(subject) + len(desc) # need page wrap? if y - (need_lines + 2 * font_size) < (bottomcontent + 2 * cm): canvas.setFont(font, font_size) # Zwischensumme canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Zwischensumme:') canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(temp_sum)) # page wrap _PageWrap(canvas) y = topcontent - font_size - line_padding * 3 # header y = _tableHead(y) odd = True # übertrag canvas.setFont(font, font_size) canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Übertrag:') canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(temp_sum)) y -= font_height + line_padding # Zwischensumme (inkl. aktueller Posten) temp_sum += el['total'] # draw the background if not odd: canvas.setFillColorRGB(0.9, 0.9, 0.9) else: canvas.setFillColorRGB(1, 1, 1) canvas.rect(left, y - (need_lines * line_height) - (2 * line_padding), height=(need_lines * line_height) + (2 * line_padding), width=right - left, fill=1, stroke=0) canvas.setFillColorRGB(0, 0, 0) y -= line_padding (integer, decimals) = _niceCount(el['count']) canvas.drawRightString(left + 0.8 * cm, y - font_height, integer) suffix = '' if decimals: suffix = ',%s' % decimals if el['unit']: suffix = suffix + ' ' + el['unit'] if suffix: canvas.drawString(left + 0.8 * cm, y - font_height, '%s' % suffix) if len(part.vat) < 2: canvas.drawString(left + 2.7 * cm, y - font_height, subject[0]) canvas.drawRightString(left + 14.3 * cm, y - font_height, _formatPrice(el['price'])) if el['tender']: canvas.drawRightString(left + 16.8 * cm, y - font_height, 'eventual') else: canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(el['total'])) subject = subject[1:] x = 1 for line in subject: canvas.drawString(left + 2.7 * cm, y - (x * line_height) - font_height, line) x += 1 for line in desc[:-1]: _drawJustifiedString(left + 2.7 * cm, y - (x * line_height) - font_height, line, canvas, 11 * cm, font, font_size) x += 1 canvas.drawString(left + 2.7 * cm, y - (x * line_height) - font_height, desc[-1]) x += 1 else: canvas.drawString(left + 2.7 * cm, y - font_height, subject[0]) canvas.drawRightString(left + 13.3 * cm, y - font_height, _formatPrice(el['price'])) canvas.drawString(left + 13.7 * cm, y - font_height, str(part.vat[el['vat']][1])) if el['tender']: canvas.drawRightString(left + 16.8 * cm, y - font_height, 'eventual') else: canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(el['total'])) subject = subject[1:] x = 1 for line in subject: canvas.drawString(left + 2.7 * cm, y - (x * line_height) - font_height, line) x += 1 for line in desc: canvas.drawString(left + 2.7 * cm, y - (x * line_height) - font_height, line) x += 1 odd = not odd y -= (need_lines * line_height) + line_padding if part.summary: y -= (0.3 * cm) if part.vatType == 'gross': summaries = [] if len(part.vat) == 1: vat = list(part.vat.keys())[0] (integer, decimals) = _niceCount((vat * 100)) vatstr = '%s' % integer if decimals: vatstr += ',%s' % decimals canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Nettobetrag:') canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(part.sum - (part.sum / (vat + 1)) * vat)) y -= line_height summaries.append(('%s%% MwSt:' % vatstr, _formatPrice((part.sum / (vat + 1)) * vat))) else: net = 0.0 for vat, vatdata in list(part.vat.items()): (integer, decimals) = _niceCount((vat * 100)) vatstr = '%s' % integer if decimals: vatstr += ',%s' % decimals _gross = vatdata[0] _net = _gross / (1+vat) _vat = _net * vat summaries.append(('%s: Teilbetrag %s: Nettobetrag %s zzgl. %s%% MwSt:' % (vatdata[1], _formatPrice(_gross), _formatPrice(_net), vatstr), _formatPrice(_vat))) net += _net summaries.append(('Nettobetrag gesamt:', _formatPrice(net))) summaries.sort() for line in summaries: canvas.drawRightString(left + 14.5 * cm, y - font_height, line[0]) canvas.drawRightString(left + 16.8 * cm, y - font_height, line[1]) y -= line_height canvas.setFont(font + '-Bold', font_size) if iv.tender: canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Gesamtbetrag:') else: canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Rechnungsbetrag:') canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(part.sum)) canvas.setFont(font, font_size) y -= line_height + line_padding else: canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Nettobetrag:') canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(part.sum)) y -= line_height summaries = [] if len(part.vat) == 1: vat = list(part.vat.keys())[0] (integer, decimals) = _niceCount((vat * 100)) vatstr = '%s' % integer if decimals: vatstr += ',%s' % decimals summaries.append(('zzgl. %s%% MwSt:' % vatstr, _formatPrice(vat * part.sum))) elif len(part.vat) > 1: for vat, vatdata in list(part.vat.items()): (integer, decimals) = _niceCount((vat * 100)) vatstr = '%s' % integer if decimals: vatstr += ',%s' % decimals summaries.append(('zzgl. %s%% MwSt (%s):' % (vatstr, vatdata[1]), _formatPrice(vat * vatdata[0]))) summaries.sort() for line in summaries: canvas.drawRightString(left + 14.5 * cm, y - font_height, line[0]) canvas.drawRightString(left + 16.8 * cm, y - font_height, line[1]) y -= line_height sum = part.sum for vat, vatdata in list(part.vat.items()): sum += vat * vatdata[0] canvas.setFont(font + '-Bold', font_size) if iv.tender: canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Gesamtbetrag:') else: canvas.drawRightString(left + 14.5 * cm, y - font_height, 'Rechnungsbetrag:') canvas.drawRightString(left + 16.8 * cm, y - font_height, _formatPrice(sum)) canvas.setFont(font, font_size) y -= line_height + line_padding elif type(part) == Invoice.Text: my_font_size = font_size canvas.setFont(font, my_font_size) left, right = leftcontent, rightcontent firsttime = True headlines = [] if part.urgent: left += 1.5 * cm right -= 1.5 * cm if part.headline: headlines = _splitToWidth(canvas, part.headline, right - left, font, my_font_size) for para in part.paragraphs: lines = _splitToWidth(canvas, para, right - left, font, my_font_size) if part.urgent: need_height = len(lines) * line_height if len(headlines) > 0: need_height += len(headlines) * (line_height + 1) + line_padding canvas.setFillColorRGB(0.95, 0.95, 0.95) canvas.rect(left - 0.5 * cm, y - (need_height + (6 * line_padding)), height=need_height + (6 * line_padding), width=right - left + 1 * cm, fill=1, stroke=1) canvas.setFillColorRGB(0, 0, 0) y -= line_padding * 3 if part.headline and firsttime: firsttime = False canvas.setFont(font + '-Bold', my_font_size + 1) for line in headlines: canvas.drawString(left, y - (font_height + 1), line) y -= line_height + 1 y -= line_padding canvas.setFont(font, my_font_size) for line in lines[:-1]: _drawJustifiedString(left, y - font_height, line, canvas, right - left, font, my_font_size) y -= line_height canvas.drawString(left, y - font_height, lines[-1]) y -= line_height y -= line_padding * 3 left, right = leftcontent, rightcontent elif type(part) == Invoice.Image: width = (part.imagedata.width / part.dpi) * inch height = width * (part.imagedata.height / part.imagedata.width) x = leftcontent if part.alignment == "center": x = leftcontent + (rightcontent - leftcontent)/2 - width/2 elif part.alignment == "right": x = rightcontent - width canvas.drawInlineImage(part.imagedata, x, y-height, width=width, height=height) y -= line_padding + height if part.caption: canvas.drawString(x, y-font_height, part.caption) y -= line_height else: raise NotImplementedError("Cannot handle part of type %s" % type(part)) y -= (0.5 * cm) canvas.showPage() canvas.save() pdfdata = fd.getvalue() return pdfdata