dinbrief 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dinbrief/__init__.py +1 -0
- dinbrief/contrib/__init__.py +0 -0
- dinbrief/contrib/form.py +112 -0
- dinbrief/contrib/qrcode.py +72 -0
- dinbrief/contrib/simple.py +15 -0
- dinbrief/document.py +26 -0
- dinbrief/invoice/__init__.py +5 -0
- dinbrief/invoice/bank_transfer_form.py +90 -0
- dinbrief/invoice/invoice.py +34 -0
- dinbrief/invoice/item.py +35 -0
- dinbrief/invoice/item_table.py +124 -0
- dinbrief/invoice/total_table.py +90 -0
- dinbrief/invoice/vat_item.py +7 -0
- dinbrief/locale/de/LC_MESSAGES/django.mo +0 -0
- dinbrief/locale/de/LC_MESSAGES/django.po +81 -0
- dinbrief/optional_django.py +20 -0
- dinbrief/styles.py +165 -0
- dinbrief/template.py +224 -0
- dinbrief-0.5.0.dist-info/LICENSE +25 -0
- dinbrief-0.5.0.dist-info/LICENSE-APACHE +202 -0
- dinbrief-0.5.0.dist-info/LICENSE-BSD +25 -0
- dinbrief-0.5.0.dist-info/LICENSE-MIT +21 -0
- dinbrief-0.5.0.dist-info/METADATA +30 -0
- dinbrief-0.5.0.dist-info/RECORD +25 -0
- dinbrief-0.5.0.dist-info/WHEEL +4 -0
dinbrief/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.4.0'
|
|
File without changes
|
dinbrief/contrib/form.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from xml.sax.saxutils import escape
|
|
4
|
+
|
|
5
|
+
from reportlab.lib import colors
|
|
6
|
+
from reportlab.lib.units import mm, cm
|
|
7
|
+
from reportlab.platypus import Paragraph
|
|
8
|
+
from reportlab.platypus import Spacer
|
|
9
|
+
from reportlab.platypus.tables import Table
|
|
10
|
+
from reportlab.platypus.tables import TableStyle
|
|
11
|
+
|
|
12
|
+
from ..optional_django import gettext as _
|
|
13
|
+
from ..styles import styles
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FlowableProxy(object):
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, name):
|
|
19
|
+
return getattr(self.flowable, name)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Field(FlowableProxy):
|
|
23
|
+
field_height = 5*mm
|
|
24
|
+
|
|
25
|
+
def __init__(self, label, field_height=None, value=None):
|
|
26
|
+
if field_height is not None:
|
|
27
|
+
self.field_height = field_height
|
|
28
|
+
self.flowable = Table(
|
|
29
|
+
data=[
|
|
30
|
+
[value or ''], # empty row for the text
|
|
31
|
+
[Paragraph(label, styles['FieldLabel'])],
|
|
32
|
+
],
|
|
33
|
+
colWidths=[80*mm],
|
|
34
|
+
rowHeights=[self.field_height, None],
|
|
35
|
+
style=TableStyle([
|
|
36
|
+
('NOSPLIT', (0, 0), (-1, -1)),
|
|
37
|
+
('TOPPADDING', (0, 0), (-1, -1), 0),
|
|
38
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
|
39
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
|
40
|
+
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
41
|
+
('LINEBELOW', (0, 0), (0, 0), 0.2*mm, colors.black),
|
|
42
|
+
#('TOPPADDING', (0, 0), (0, 0), 5*mm),
|
|
43
|
+
]),
|
|
44
|
+
hAlign='LEFT'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SignatureField(Field):
|
|
49
|
+
field_height = 15*mm
|
|
50
|
+
|
|
51
|
+
def __init__(self, label=None, field_height=None, value=None):
|
|
52
|
+
if label is None:
|
|
53
|
+
label = _('Place, date and signature')
|
|
54
|
+
super(SignatureField, self).__init__(label, field_height, value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TwoSignaturesField(FlowableProxy):
|
|
58
|
+
field_height = 15*mm
|
|
59
|
+
|
|
60
|
+
def __init__(self, label_left, label_right, field_height=None,
|
|
61
|
+
value_left=None, value_right=None):
|
|
62
|
+
if field_height is not None:
|
|
63
|
+
self.field_height = field_height
|
|
64
|
+
self.flowable = Table(
|
|
65
|
+
data=[
|
|
66
|
+
[value_left or '', '', value_right or ''], # empty row for the text
|
|
67
|
+
[
|
|
68
|
+
Paragraph(label_left, styles['FieldLabel']),
|
|
69
|
+
'',
|
|
70
|
+
Paragraph(label_right, styles['FieldLabel']),
|
|
71
|
+
],
|
|
72
|
+
],
|
|
73
|
+
colWidths=[80*mm, 10*mm, 80*mm],
|
|
74
|
+
rowHeights=[self.field_height, None],
|
|
75
|
+
style=TableStyle([
|
|
76
|
+
('NOSPLIT', (0, 0), (-1, -1)),
|
|
77
|
+
('TOPPADDING', (0, 0), (-1, -1), 0),
|
|
78
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
|
79
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
|
80
|
+
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
81
|
+
('LINEBELOW', (0, 0), (0, 0), 0.2*mm, colors.black),
|
|
82
|
+
('LINEBELOW', (2, 0), (2, 0), 0.2*mm, colors.black),
|
|
83
|
+
]),
|
|
84
|
+
hAlign='LEFT'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PostalCodeAndCityField(FlowableProxy):
|
|
89
|
+
field_height = 5*mm
|
|
90
|
+
def __init__(self):
|
|
91
|
+
self.flowable = Table(
|
|
92
|
+
data=[
|
|
93
|
+
['', '', ''], # empty row for the text
|
|
94
|
+
[
|
|
95
|
+
Paragraph(_('Postal code'), styles['FieldLabel']),
|
|
96
|
+
'', # Space between postal code and city
|
|
97
|
+
Paragraph(_('City'), styles['FieldLabel']),
|
|
98
|
+
],
|
|
99
|
+
],
|
|
100
|
+
colWidths=[20*mm, 5*mm, 55*mm],
|
|
101
|
+
rowHeights=[self.field_height, None],
|
|
102
|
+
style=TableStyle([
|
|
103
|
+
('NOSPLIT', (0, 0), (-1, -1)),
|
|
104
|
+
('TOPPADDING', (0, 0), (-1, -1), 0),
|
|
105
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
|
106
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 0),
|
|
107
|
+
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
108
|
+
('LINEBELOW', (0, 0), (0, 0), 0.2*mm, colors.black),
|
|
109
|
+
('LINEBELOW', (2, 0), (2, 0), 0.2*mm, colors.black),
|
|
110
|
+
]),
|
|
111
|
+
hAlign='LEFT'
|
|
112
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
except ImportError:
|
|
6
|
+
from StringIO import StringIO as BytesIO
|
|
7
|
+
|
|
8
|
+
import qrcode
|
|
9
|
+
from reportlab.platypus.flowables import Image
|
|
10
|
+
|
|
11
|
+
from ..styles import styles
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def qrcode_image(data):
|
|
15
|
+
# create QRCode object
|
|
16
|
+
code = qrcode.QRCode(
|
|
17
|
+
box_size=1,
|
|
18
|
+
border=0,
|
|
19
|
+
error_correction=qrcode.constants.ERROR_CORRECT_M)
|
|
20
|
+
code.add_data(data)
|
|
21
|
+
code.make()
|
|
22
|
+
# render QRCode as PNG into memory
|
|
23
|
+
img = code.make_image()
|
|
24
|
+
img_data = BytesIO()
|
|
25
|
+
img.save(img_data, 'PNG')
|
|
26
|
+
img_data.seek(0)
|
|
27
|
+
img_data.__repr__ = lambda: 'qrcode.png'
|
|
28
|
+
# create image floatable
|
|
29
|
+
return Image(img_data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sepa_credit_transfer(account_holder, iban, bic, amount, reference,
|
|
33
|
+
purpose='', currency='EUR'):
|
|
34
|
+
'''
|
|
35
|
+
Create QRCode object according to EPC069-12:
|
|
36
|
+
http://www.europeanpaymentscouncil.eu/knowledge_bank_detail.cfm?documents_id=607
|
|
37
|
+
A list of purpose options can be found online:
|
|
38
|
+
http://www.hettwer-beratung.de/sepa-spezialwissen/sepa-technische-anforderungen/sepa-purpose-codes-vs-dta-textschl%C3%BCssel/
|
|
39
|
+
'''
|
|
40
|
+
assert 1 <= len(account_holder) < 70
|
|
41
|
+
assert len(currency) == 3
|
|
42
|
+
assert len(bic) in (8, 11)
|
|
43
|
+
assert ' ' not in bic
|
|
44
|
+
assert 1 <= len(iban) <= 34
|
|
45
|
+
assert ' ' not in iban
|
|
46
|
+
assert len(purpose) <= 4
|
|
47
|
+
assert len(reference) <= 35
|
|
48
|
+
return qrcode_image('\n'.join((
|
|
49
|
+
# Service Tag
|
|
50
|
+
'BCD',
|
|
51
|
+
# Version
|
|
52
|
+
'001',
|
|
53
|
+
# Character set (1=utf-8)
|
|
54
|
+
'1',
|
|
55
|
+
# identification code
|
|
56
|
+
'SCT',
|
|
57
|
+
# BIC
|
|
58
|
+
bic,
|
|
59
|
+
# name
|
|
60
|
+
account_holder,
|
|
61
|
+
# IBAN
|
|
62
|
+
iban,
|
|
63
|
+
# Amount
|
|
64
|
+
'%s%.2f' % (currency, amount),
|
|
65
|
+
# Purpose
|
|
66
|
+
purpose,
|
|
67
|
+
# Reference
|
|
68
|
+
reference,
|
|
69
|
+
# Unstructured Remittance Information
|
|
70
|
+
'',
|
|
71
|
+
# Beneficiary to originator information
|
|
72
|
+
'')))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
|
|
3
|
+
from reportlab.lib.units import mm
|
|
4
|
+
from reportlab.platypus import Paragraph
|
|
5
|
+
from reportlab.platypus import Spacer
|
|
6
|
+
|
|
7
|
+
from ..styles import styles
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
P = partial(Paragraph, style=styles['Text'])
|
|
11
|
+
H2 = partial(Paragraph, style=styles['H2'])
|
|
12
|
+
Subject = partial(Paragraph, style=styles['Subject'])
|
|
13
|
+
SmallSpacer = partial(Spacer, width=0, height=5*mm)
|
|
14
|
+
MediumSpacer = partial(Spacer, width=0, height=10*mm)
|
|
15
|
+
LargeSpacer = partial(Spacer, width=0, height=20*mm)
|
dinbrief/document.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Document(object):
|
|
2
|
+
|
|
3
|
+
title = ''
|
|
4
|
+
subject = ''
|
|
5
|
+
author = ''
|
|
6
|
+
keywords = None
|
|
7
|
+
creator = 'http://pypi.python.org/pypi/dinbrief'
|
|
8
|
+
|
|
9
|
+
sender = None
|
|
10
|
+
recipient = None
|
|
11
|
+
content = None
|
|
12
|
+
date = ''
|
|
13
|
+
|
|
14
|
+
def __init__(self, **kwargs):
|
|
15
|
+
# Metadata
|
|
16
|
+
self.title = kwargs.pop('title', self.title)
|
|
17
|
+
self.subject = kwargs.pop('subject', self.subject)
|
|
18
|
+
self.author = kwargs.pop('author', self.author)
|
|
19
|
+
self.keywords = kwargs.pop('keywords', self.keywords)
|
|
20
|
+
self.creator = kwargs.pop('creator', self.creator)
|
|
21
|
+
# Content
|
|
22
|
+
self.sender = kwargs.pop('sender', self.sender or [])
|
|
23
|
+
self.recipient = kwargs.pop('recipient', self.recipient or [])
|
|
24
|
+
self.infobox = kwargs.pop('infobox', [])
|
|
25
|
+
self.date = kwargs.pop('date', self.date)
|
|
26
|
+
self.content = kwargs.pop('content', self.content or [])
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from xml.sax.saxutils import escape
|
|
3
|
+
|
|
4
|
+
from reportlab.lib import colors
|
|
5
|
+
from reportlab.lib.units import mm, cm
|
|
6
|
+
from reportlab.platypus import Paragraph
|
|
7
|
+
from reportlab.platypus import Spacer
|
|
8
|
+
from reportlab.platypus.tables import Table
|
|
9
|
+
from reportlab.platypus.tables import TableStyle
|
|
10
|
+
|
|
11
|
+
from ..optional_django import gettext as _
|
|
12
|
+
from ..optional_django import number_format
|
|
13
|
+
from ..styles import styles
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def BankTransferForm(account_holder, iban, bic, reference, amount,
|
|
17
|
+
currency=u'EUR', currency_label=u'€', show_qrcode=False):
|
|
18
|
+
|
|
19
|
+
col_widths = [40*mm, 60*mm]
|
|
20
|
+
row_heights = [7*mm, 6*mm, 6*mm, 6*mm, 7*mm]
|
|
21
|
+
|
|
22
|
+
table_style = []
|
|
23
|
+
|
|
24
|
+
if show_qrcode:
|
|
25
|
+
from ..contrib import qrcode
|
|
26
|
+
table_style += [
|
|
27
|
+
('SPAN', (2, 0), (2, 4)),
|
|
28
|
+
]
|
|
29
|
+
qrcode_size = sum(row_heights) - 4*mm
|
|
30
|
+
col_widths += [qrcode_size + 4*mm]
|
|
31
|
+
qrcode_image = qrcode.sepa_credit_transfer(
|
|
32
|
+
account_holder=account_holder,
|
|
33
|
+
iban=''.join(iban.split()),
|
|
34
|
+
bic=''.join(bic.split()),
|
|
35
|
+
reference=reference,
|
|
36
|
+
amount=amount,
|
|
37
|
+
currency=currency)
|
|
38
|
+
qrcode_image.drawWidth = qrcode_size
|
|
39
|
+
qrcode_image.drawHeight = qrcode_size
|
|
40
|
+
|
|
41
|
+
table_style += [
|
|
42
|
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
43
|
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
44
|
+
('TOPPADDING', (0, 0), (-1, -1), 1*mm),
|
|
45
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 2*mm),
|
|
46
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 1*mm),
|
|
47
|
+
('LEFTPADDING', (0, 0), (-1, -1), 2*mm),
|
|
48
|
+
('NOSPLIT', (0, 0), (-1, -1)),
|
|
49
|
+
# right
|
|
50
|
+
('RIGHTPADDING', (-1, 0), (-1, -1), 2*mm),
|
|
51
|
+
('LINEAFTER', (-1, 0), (-1, -1), 0.2*mm, colors.black),
|
|
52
|
+
# left
|
|
53
|
+
('LEFTPADDING', (0, 0), (0, -1), 2*mm),
|
|
54
|
+
('LINEBEFORE', (0, 0), (0, -1), 0.2*mm, colors.black),
|
|
55
|
+
# top
|
|
56
|
+
('TOPPADDING', (0, 0), (-1, 0), 2*mm),
|
|
57
|
+
('LINEABOVE', (0, 0), (-1, 0), 0.2*mm, colors.black),
|
|
58
|
+
# bottom
|
|
59
|
+
('BOTTOMPADDING', (0, -1), (-1, -1), 3*mm),
|
|
60
|
+
('LINEBELOW', (0, -1), (-1, -1), 0.2*mm, colors.black),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
def data_generator():
|
|
64
|
+
yield (
|
|
65
|
+
Paragraph(_(u'Account holder') + ':', styles['TableCell']),
|
|
66
|
+
Paragraph(account_holder, styles['TableCell'])
|
|
67
|
+
) + ((qrcode_image,) if show_qrcode else ())
|
|
68
|
+
yield (
|
|
69
|
+
Paragraph(_(u'IBAN') + ':', styles['TableCell']),
|
|
70
|
+
Paragraph(iban, styles['TableCell']),
|
|
71
|
+
)
|
|
72
|
+
yield (
|
|
73
|
+
Paragraph(_(u'BIC') + ':', styles['TableCell']),
|
|
74
|
+
Paragraph(bic, styles['TableCell']),
|
|
75
|
+
)
|
|
76
|
+
yield (
|
|
77
|
+
Paragraph(_(u'Amount') + ':', styles['TableCell']),
|
|
78
|
+
Paragraph(u'%s %s' % (number_format(amount, 2), currency_label), styles['TableCell']),
|
|
79
|
+
)
|
|
80
|
+
yield (
|
|
81
|
+
Paragraph(_(u'Reference') + ':', styles['TableCell']),
|
|
82
|
+
Paragraph(reference, styles['TableCell']),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return Table(
|
|
86
|
+
data=list(data_generator()),
|
|
87
|
+
colWidths=col_widths,
|
|
88
|
+
rowHeights=row_heights,
|
|
89
|
+
style=TableStyle(table_style),
|
|
90
|
+
hAlign='LEFT')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from .vat_item import VatItem
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Invoice(object):
|
|
7
|
+
|
|
8
|
+
def __init__(self, items=None, currency=u'€'):
|
|
9
|
+
self.items = items or []
|
|
10
|
+
self.currency = currency
|
|
11
|
+
self.vat_items = []
|
|
12
|
+
self.recalculate()
|
|
13
|
+
|
|
14
|
+
def recalculate(self):
|
|
15
|
+
self.vat_items = []
|
|
16
|
+
d = {}
|
|
17
|
+
for item in self.items:
|
|
18
|
+
if not item.vat_rate:
|
|
19
|
+
continue
|
|
20
|
+
try:
|
|
21
|
+
vat_item = d[item.vat_rate]
|
|
22
|
+
except KeyError:
|
|
23
|
+
vat_item = VatItem(rate=item.vat_rate)
|
|
24
|
+
d[item.vat_rate] = vat_item
|
|
25
|
+
vat_item.amount += item.vat_rate * item.subtotal
|
|
26
|
+
self.vat_items = sorted(d.values(), key=lambda item: item.rate)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def gross(self):
|
|
30
|
+
return self.net + sum(vat_item.amount for vat_item in self.vat_items)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def net(self):
|
|
34
|
+
return sum(item.total for item in self.items)
|
dinbrief/invoice/item.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Item(object):
|
|
5
|
+
|
|
6
|
+
def __init__(self, position=0, text='', period='', date=None,
|
|
7
|
+
price=Decimal(0), unit='', quantity=Decimal(1),
|
|
8
|
+
discount=Decimal(0), vat_rate=Decimal(0),
|
|
9
|
+
type='item'):
|
|
10
|
+
self.position = position
|
|
11
|
+
self.text = text
|
|
12
|
+
self.period = period
|
|
13
|
+
self.date = date
|
|
14
|
+
self.price = price
|
|
15
|
+
self.unit = unit
|
|
16
|
+
self.quantity = quantity
|
|
17
|
+
self.discount = discount
|
|
18
|
+
self.vat_rate = vat_rate
|
|
19
|
+
self.type = type
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def subtotal(self):
|
|
23
|
+
return self.price * self.quantity
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def discount_percentage(self):
|
|
27
|
+
return self.discount * 100
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def discount_amount(self):
|
|
31
|
+
return self.discount * self.subtotal
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def total(self):
|
|
35
|
+
return self.subtotal - self.discount_amount
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from functools import partial
|
|
3
|
+
from xml.sax.saxutils import escape
|
|
4
|
+
|
|
5
|
+
from reportlab.lib import colors
|
|
6
|
+
from reportlab.lib.units import mm, cm
|
|
7
|
+
from reportlab.platypus import Paragraph
|
|
8
|
+
from reportlab.platypus.tables import Table
|
|
9
|
+
from reportlab.platypus.tables import TableStyle
|
|
10
|
+
|
|
11
|
+
from ..optional_django import gettext as _
|
|
12
|
+
from ..optional_django import number_format
|
|
13
|
+
from ..optional_django import date_format
|
|
14
|
+
from ..styles import styles
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Head = partial(Paragraph, style=styles['TableHead'])
|
|
18
|
+
HeadRight = partial(Paragraph, style=styles['TableHeadRight'])
|
|
19
|
+
Cell = partial(Paragraph, style=styles['TableCell'])
|
|
20
|
+
Number = partial(Paragraph, style=styles['TableNumber'])
|
|
21
|
+
Title = partial(Paragraph, style=styles['TableTitle'])
|
|
22
|
+
|
|
23
|
+
title_bgcolor = colors.CMYKColor(black=0.1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ItemTable(brief_template, invoice):
|
|
27
|
+
|
|
28
|
+
style = [
|
|
29
|
+
('VALIGN', (0, 0), (-1, 0), 'BOTTOM'),
|
|
30
|
+
('VALIGN', (0, 1), (-1, -1), 'TOP'),
|
|
31
|
+
('LINEBELOW', (0, 0), (-1, 0), 0.3*mm, colors.black),
|
|
32
|
+
#('LINEBELOW', (0, 1), (-1, -1), 0.1*mm, colors.black),
|
|
33
|
+
('TOPPADDING', (0, 0), (-1, -1), 2*mm),
|
|
34
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 2*mm),
|
|
35
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 3*mm),
|
|
36
|
+
('LEFTPADDING', (0, 0), (-1, -1), 2*mm),
|
|
37
|
+
# no padding on the far left and far right
|
|
38
|
+
('LEFTPADDING', (0, 0), (0, -1), 0),
|
|
39
|
+
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
|
|
40
|
+
# no padding between net price and unit
|
|
41
|
+
('RIGHTPADDING', (3, 0), (3, -1), 0),
|
|
42
|
+
('LEFTPADDING', (4, 0), (4, -1), 0),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
show_period_column = any(item.period for item in invoice.items)
|
|
46
|
+
show_date_column = any(item.date for item in invoice.items) and \
|
|
47
|
+
not show_period_column
|
|
48
|
+
|
|
49
|
+
col_widths = [
|
|
50
|
+
8*mm, # position
|
|
51
|
+
0, # description
|
|
52
|
+
46*mm if show_period_column else # period
|
|
53
|
+
24*mm if show_date_column else # date
|
|
54
|
+
0, # neither period nor date
|
|
55
|
+
21*mm, # unit price
|
|
56
|
+
13*mm, # unit
|
|
57
|
+
20*mm, # quantity
|
|
58
|
+
26*mm # sum price
|
|
59
|
+
]
|
|
60
|
+
col_widths[1] = brief_template.CONTENT_WIDTH - sum(col_widths)
|
|
61
|
+
|
|
62
|
+
def data_generator():
|
|
63
|
+
# header
|
|
64
|
+
yield (
|
|
65
|
+
HeadRight(u'#'),
|
|
66
|
+
Head(_('Description')),
|
|
67
|
+
Head(_('Period') if show_period_column else
|
|
68
|
+
_('Date') if show_date_column else ''),
|
|
69
|
+
HeadRight(_('Unit Price')),
|
|
70
|
+
Head(u''),
|
|
71
|
+
HeadRight(_('Quantity')),
|
|
72
|
+
HeadRight(_('Line Total')),
|
|
73
|
+
)
|
|
74
|
+
# items
|
|
75
|
+
row = 0
|
|
76
|
+
for item in invoice.items:
|
|
77
|
+
row += 1
|
|
78
|
+
item_type = getattr(item, 'type', 'item')
|
|
79
|
+
if item_type == 'item':
|
|
80
|
+
if not (item.period or item.date):
|
|
81
|
+
style.append(('SPAN', (1, row), (2, row)))
|
|
82
|
+
yield (
|
|
83
|
+
Number(u'%s' % item.position),
|
|
84
|
+
Cell(escape(item.text)),
|
|
85
|
+
Cell(escape(item.period) if item.period else
|
|
86
|
+
escape(date_format(item.date, 'SHORT_DATE_FORMAT')) if item.date else u''),
|
|
87
|
+
Number(u'%s €' % number_format(item.price, 2)),
|
|
88
|
+
Cell((u'/%s' % escape(item.unit))
|
|
89
|
+
if item.unit else u''),
|
|
90
|
+
Number(number_format(item.quantity, 2)),
|
|
91
|
+
Number(u'%s €' % number_format(item.subtotal, 2)),
|
|
92
|
+
)
|
|
93
|
+
if item.discount:
|
|
94
|
+
row += 1
|
|
95
|
+
percentage = number_format(item.discount_percentage)
|
|
96
|
+
yield (
|
|
97
|
+
Cell(u''),
|
|
98
|
+
Cell((u'%s%% ' % percentage) + _('discount')),
|
|
99
|
+
Cell(u''),
|
|
100
|
+
Cell(u''),
|
|
101
|
+
Cell(u''),
|
|
102
|
+
Cell(u''),
|
|
103
|
+
Number(u'–%s €' % number_format(item.discount_amount, 2)),
|
|
104
|
+
)
|
|
105
|
+
style.append(('TOPPADDING', (0, row), (-1, row), 0))
|
|
106
|
+
# draw line below item
|
|
107
|
+
style.append(('LINEBELOW', (0, row), (-1, row),
|
|
108
|
+
0.1*mm, colors.black))
|
|
109
|
+
elif item_type == 'title':
|
|
110
|
+
style.append(('SPAN', (1, row), (6, row)))
|
|
111
|
+
style.append(('BACKGROUND', (0, row), (-1, row),
|
|
112
|
+
title_bgcolor))
|
|
113
|
+
style.append(('LINEBELOW', (0, row), (-1, row),
|
|
114
|
+
0.1*mm, colors.black))
|
|
115
|
+
yield (
|
|
116
|
+
Number(u'%s' % item.position),
|
|
117
|
+
Title(escape(item.text)),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return Table(
|
|
121
|
+
data=list(data_generator()),
|
|
122
|
+
colWidths=col_widths,
|
|
123
|
+
style=TableStyle(style),
|
|
124
|
+
repeatRows=1)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from xml.sax.saxutils import escape
|
|
3
|
+
|
|
4
|
+
from reportlab.lib import colors
|
|
5
|
+
from reportlab.lib.units import mm, cm
|
|
6
|
+
from reportlab.platypus import Paragraph
|
|
7
|
+
from reportlab.platypus import Spacer
|
|
8
|
+
from reportlab.platypus.tables import Table
|
|
9
|
+
from reportlab.platypus.tables import TableStyle
|
|
10
|
+
|
|
11
|
+
from ..optional_django import gettext as _
|
|
12
|
+
from ..optional_django import number_format
|
|
13
|
+
from ..styles import styles
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def TotalTable(brief_template, invoice):
|
|
17
|
+
|
|
18
|
+
table_style = [
|
|
19
|
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
20
|
+
('TOPPADDING', (0, 0), (-1, -1), 1*mm),
|
|
21
|
+
('RIGHTPADDING', (0, 0), (-1, -1), 2*mm),
|
|
22
|
+
('BOTTOMPADDING', (0, 0), (-1, -1), 1*mm),
|
|
23
|
+
('LEFTPADDING', (0, 0), (-1, -1), 2*mm),
|
|
24
|
+
# no padding on the far left and far right
|
|
25
|
+
('LEFTPADDING', (0, 0), (0, -1), 0),
|
|
26
|
+
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
col_widths = [0, 32*mm, 24*mm]
|
|
30
|
+
col_widths[0] = brief_template.CONTENT_WIDTH - sum(col_widths)
|
|
31
|
+
|
|
32
|
+
if invoice.vat_items:
|
|
33
|
+
net_row = 0
|
|
34
|
+
vat_row = net_row+1
|
|
35
|
+
gross_row = vat_row + len(invoice.vat_items)
|
|
36
|
+
table_style += [
|
|
37
|
+
('BOTTOMPADDING', (0, gross_row-1), (-1, gross_row-1), 2*mm),
|
|
38
|
+
#('TOPPADDING', (0, vat_row), (-1, gross_row), 1*mm),
|
|
39
|
+
#('BOTTOMPADDING', (0, vat_row), (-1, gross_row), 1*mm),
|
|
40
|
+
('LINEABOVE', (0, net_row), (-1, net_row), 0.3*mm, colors.black),
|
|
41
|
+
('LINEABOVE', (1, gross_row), (-1, gross_row), 0.3*mm, colors.black),
|
|
42
|
+
]
|
|
43
|
+
def data_generator():
|
|
44
|
+
yield (
|
|
45
|
+
Paragraph(u'', styles['TableCell']),
|
|
46
|
+
Paragraph(_(u'Sum (net)'), styles['TableCell']),
|
|
47
|
+
Paragraph(u'%s €' % number_format(invoice.net, 2),
|
|
48
|
+
styles['TableNumber']),
|
|
49
|
+
)
|
|
50
|
+
for vat_item in invoice.vat_items:
|
|
51
|
+
yield (
|
|
52
|
+
Paragraph(u'', styles['TableCell']),
|
|
53
|
+
Paragraph((u'+%s%% ' % number_format(vat_item.rate * 100)) +
|
|
54
|
+
_('VAT'), styles['TableCell']),
|
|
55
|
+
Paragraph(u'%s €' % number_format(vat_item.amount, 2),
|
|
56
|
+
styles['TableNumber']),
|
|
57
|
+
)
|
|
58
|
+
yield (
|
|
59
|
+
Paragraph(u'', styles['TableCell']),
|
|
60
|
+
Paragraph(_(u'Sum (gross)'), styles['GrossTableCell']),
|
|
61
|
+
Paragraph(u'%s €' % number_format(invoice.gross, 2),
|
|
62
|
+
styles['GrossValueTableCell']),
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
net_row = 0
|
|
66
|
+
gross_row = 1
|
|
67
|
+
table_style += [
|
|
68
|
+
('TOPPADDING', (0, net_row), (-1, net_row), 2*mm),
|
|
69
|
+
('BOTTOMPADDING', (0, net_row), (-1, net_row), 0),
|
|
70
|
+
('LINEABOVE', (0, net_row), (-1, net_row), 0.3*mm, colors.black),
|
|
71
|
+
('TOPPADDING', (0, gross_row), (-1, gross_row), 2*mm),
|
|
72
|
+
('LINEABOVE', (1, gross_row), (-1, gross_row), 0.3*mm, colors.black),
|
|
73
|
+
]
|
|
74
|
+
def data_generator():
|
|
75
|
+
yield (
|
|
76
|
+
Spacer(0, 0),
|
|
77
|
+
Spacer(0, 0),
|
|
78
|
+
Spacer(0, 0),
|
|
79
|
+
)
|
|
80
|
+
yield (
|
|
81
|
+
Paragraph(u'', styles['TableCell']),
|
|
82
|
+
Paragraph(_(u'Sum (net)'), styles['GrossTableCell']),
|
|
83
|
+
Paragraph(u'%s €' % number_format(invoice.gross, 2),
|
|
84
|
+
styles['GrossValueTableCell']),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return Table(
|
|
88
|
+
data=list(data_generator()),
|
|
89
|
+
colWidths=col_widths,
|
|
90
|
+
style=TableStyle(table_style))
|
|
Binary file
|