8078fef385bde3ea65c68bf596d2624ed63aa3f9
Bernd Wurst WiP

Bernd Wurst authored 9 months ago

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) 
Bernd Wurst WiP

Bernd Wurst authored 9 months ago

24) from .InvoiceObjects import InvoiceTable, InvoiceText, InvoiceImage, GUTSCHRIFT
Bernd Wurst WiP

Bernd Wurst authored 9 months ago

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
Bernd Wurst WiP

Bernd Wurst authored 9 months ago

311)         if self.invoice.seller['phone']:
312)             self.canvas.drawString(self.rightcolumn, self.y, f"Tel: {self.invoice.seller['phone']}")
313)             self.y -= (self.font_size + 5)
314)         if self.invoice.seller['email']:
315)             self.canvas.drawString(self.rightcolumn, self.y, f"E-Mail: {self.invoice.seller['email']}")
316)             self.y -= (self.font_size + 10)
Bernd Wurst WiP

Bernd Wurst authored 9 months ago

317)         self.y = -9.5*cm
318) 
319) 
320)     def title(self, title):
321)         self.canvas.setTitle(title)
322)         self.canvas.drawString(self.leftcontent, self.y, title)
323) 
324) 
325)     def renderRechnung(self):
326)         self.firstPage()
327)         self.canvas.setFont(self.font+'-Bold', self.font_size+3)
328)         self.title(self.invoice.title)
329) 
330)         if self.invoice.tender:
331)             self.canvas.setFont(self.font, self.font_size)
332)             self.canvas.drawString(self.rightcolumn, self.y, "Erstellungsdatum:")
333)             self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.date.strftime('%d. %m. %Y'))
334)             self.y -= (self.font_size + 0.1*cm)
335)         else:
336)             self.canvas.setFont(self.font+'-Bold', self.font_size)
337)             self.canvas.drawString(self.rightcolumn, self.y, "Bei Fragen bitte immer angeben:")
338)             self.y -= (self.font_size + 0.2*cm)
339)             self.canvas.setFont(self.font, self.font_size)
340)             self.canvas.drawString(self.rightcolumn, self.y, "Rechnungsdatum:")
341)             self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.date.strftime('%d. %m. %Y'))
342)             self.y -= (self.font_size + 0.1*cm)
343)             self.canvas.drawString(self.rightcolumn, self.y, "Rechnungsnummer:")
344)             self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.id)
345)             self.y -= (self.font_size + 0.1*cm)
346)         if self.invoice.customerno:
347)             self.canvas.drawString(self.rightcolumn, self.y, "Kundennummer:")
348)             self.canvas.drawRightString(self.rightcontent, self.y, "%s" % self.invoice.customerno)
349)             self.y -= (self.font_size + 0.5*cm)
350)         self.canvas.setFont(self.font, self.font_size)
351)         
352)         if self.invoice.salutation:
353)             self.canvas.drawString(self.leftcontent, self.y, self.invoice.salutation)
354)             self.y -= self.font_size + 0.2*cm
355)             introText = 'hiermit stellen wir Ihnen die nachfolgend genannten Leistungen in Rechnung.'
356)             if self.invoice.tender:
357)                 introText = 'hiermit unterbreiten wir Ihnen folgendes Angebot.'
Bernd Wurst WiP

Bernd Wurst authored 9 months ago

358)             if self.invoice.type == GUTSCHRIFT:
359)                 introText = 'nach unserer Berechnung entsteht für Sie folgende Gutschrift.'