PyInvoice/pyinvoice/templates.py

314 lines
12 KiB
Python
Raw Normal View History

from __future__ import unicode_literals
from datetime import datetime, date
2015-06-09 14:20:04 +00:00
from decimal import Decimal
2015-06-09 16:53:08 +00:00
2015-06-08 16:30:47 +00:00
from reportlab.lib import colors
2015-06-06 09:44:18 +00:00
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
2015-05-27 17:09:44 +00:00
from reportlab.lib.pagesizes import letter
2015-05-30 16:30:00 +00:00
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
2015-05-27 17:09:44 +00:00
from reportlab.lib.units import inch
2015-06-09 16:53:08 +00:00
from reportlab.platypus import SimpleDocTemplate, Paragraph, Table, Spacer
2015-06-06 09:51:52 +00:00
2015-06-06 09:00:31 +00:00
from pyinvoice.components import SimpleTable, TableWithHeader, PaidStamp
from pyinvoice.models import PDFInfo, Item, Transaction, InvoiceInfo, ServiceProviderInfo, ClientInfo
2015-05-27 17:09:44 +00:00
class SimpleInvoice(SimpleDocTemplate):
default_pdf_info = PDFInfo(title='Invoice', author='CiCiApp.com', subject='Invoice')
precision = None
def __init__(self, invoice_path, pdf_info=None, precision='0.01'):
if not pdf_info:
pdf_info = self.default_pdf_info
2015-05-27 17:09:44 +00:00
SimpleDocTemplate.__init__(
self,
invoice_path,
pagesize=letter,
2015-05-28 10:17:55 +00:00
rightMargin=inch,
leftMargin=inch,
topMargin=inch,
bottomMargin=inch,
**pdf_info.__dict__
)
self.precision = precision
2015-06-06 09:44:18 +00:00
self._defined_styles = getSampleStyleSheet()
self._defined_styles.add(
ParagraphStyle('RightHeading1', parent=self._defined_styles.get('Heading1'), alignment=TA_RIGHT)
)
self._defined_styles.add(
2015-06-08 16:30:47 +00:00
ParagraphStyle('TableParagraph', parent=self._defined_styles.get('Normal'), alignment=TA_CENTER)
2015-06-06 09:44:18 +00:00
)
self.invoice_info = None
self.service_provider_info = None
self.client_info = None
2015-06-06 09:00:31 +00:00
self.is_paid = False
self._items = []
2015-06-09 14:20:04 +00:00
self._item_tax_rate = None
self._transactions = []
2015-06-06 09:31:18 +00:00
self._story = []
2015-06-09 16:53:08 +00:00
self._bottom_tip = None
self._bottom_tip_align = None
@property
def items(self):
return self._items[:]
def add_item(self, item):
if isinstance(item, Item):
self._items.append(item)
2015-06-09 14:20:04 +00:00
def set_item_tax_rate(self, rate):
self._item_tax_rate = rate
@property
def transactions(self):
return self._transactions[:]
def add_transaction(self, t):
if isinstance(t, Transaction):
self._transactions.append(t)
2015-06-09 16:53:08 +00:00
def set_bottom_tip(self, text, align=TA_CENTER):
self._bottom_tip = text
self._bottom_tip_align = align
@staticmethod
def _format_value(value):
2015-06-06 09:17:57 +00:00
if isinstance(value, datetime):
value = value.strftime('%Y-%m-%d %H:%M')
elif isinstance(value, date):
value = value.strftime('%Y-%m-%d')
return value
def _attribute_to_table_data(self, instance, attribute_verbose_name_list):
data = []
2015-06-06 09:17:57 +00:00
for property_name, verbose_name in attribute_verbose_name_list:
attr = getattr(instance, property_name)
if attr:
attr = self._format_value(attr)
2015-06-06 09:17:57 +00:00
data.append(['{0}:'.format(verbose_name), attr])
return data
2015-06-11 13:06:35 +00:00
def _invoice_info_data(self):
if isinstance(self.invoice_info, InvoiceInfo):
props = [('invoice_id', 'Invoice id'), ('invoice_datetime', 'Invoice date'),
('due_datetime', 'Invoice due date')]
2015-06-11 13:06:35 +00:00
return self._attribute_to_table_data(self.invoice_info, props)
return []
def _build_invoice_info(self):
invoice_info_data = self._invoice_info_data()
if invoice_info_data:
self._story.append(Paragraph('Invoice', self._defined_styles.get('RightHeading1')))
self._story.append(SimpleTable(invoice_info_data, horizontal_align='RIGHT'))
def _service_provider_data(self):
2015-06-11 13:23:28 +00:00
if isinstance(self.service_provider_info, ServiceProviderInfo):
props = [('name', 'Name'), ('street', 'Street'), ('city', 'City'), ('state', 'State'),
('country', 'Country'), ('post_code', 'Post code'), ('vat_tax_number', 'Vat/Tax number')]
return self._attribute_to_table_data(self.service_provider_info, props)
2015-06-08 15:55:05 +00:00
2015-06-11 13:23:28 +00:00
return []
2015-06-08 15:55:05 +00:00
def _build_service_provider_info(self):
2015-06-08 15:55:05 +00:00
# Merchant
2015-06-11 13:23:28 +00:00
service_provider_info_data = self._service_provider_data()
if service_provider_info_data:
2015-06-11 14:32:19 +00:00
self._story.append(Paragraph('Service Provider', self._defined_styles.get('RightHeading1')))
2015-06-11 13:23:28 +00:00
self._story.append(SimpleTable(service_provider_info_data, horizontal_align='RIGHT'))
def _client_info_data(self):
2015-06-11 13:52:06 +00:00
if not isinstance(self.client_info, ClientInfo):
return []
2015-06-08 15:55:05 +00:00
props = [('name', 'Name'), ('street', 'Street'), ('city', 'City'), ('state', 'State'),
('country', 'Country'), ('post_code', 'Post code'), ('email', 'Email'), ('client_id', 'Client id')]
return self._attribute_to_table_data(self.client_info, props)
2015-06-08 15:55:05 +00:00
def _build_client_info(self):
2015-06-06 09:17:57 +00:00
# ClientInfo
2015-06-11 13:52:06 +00:00
client_info_data = self._client_info_data()
if client_info_data:
self._story.append(Paragraph('Client', self._defined_styles.get('Heading1')))
self._story.append(SimpleTable(client_info_data, horizontal_align='LEFT'))
2015-06-08 15:55:05 +00:00
def _build_service_provider_and_client_info(self):
2015-06-08 15:55:05 +00:00
if isinstance(self.service_provider_info, ServiceProviderInfo) and isinstance(self.client_info, ClientInfo):
# Merge Table
table_data = [
2015-06-11 13:52:06 +00:00
[
Paragraph('Service Provider', self._defined_styles.get('Heading1')), '',
'',
Paragraph('Client', self._defined_styles.get('Heading1')), ''
]
2015-06-08 15:55:05 +00:00
]
table_style = [
('SPAN', (0, 0), (1, 0)),
2015-06-08 16:30:47 +00:00
('SPAN', (3, 0), (4, 0)),
('LINEBELOW', (0, 0), (1, 0), 1, colors.gray),
('LINEBELOW', (3, 0), (4, 0), 1, colors.gray),
('LEFTPADDING', (0, 0), (-1, -1), 0),
2015-06-08 15:55:05 +00:00
]
client_info_data = self._client_info_data()
service_provider_data = self._service_provider_data()
2015-06-08 15:55:05 +00:00
diff = abs(len(client_info_data) - len(service_provider_data))
if diff > 0:
if len(client_info_data) < len(service_provider_data):
client_info_data.extend([["", ""]]*diff)
else:
service_provider_data.extend([["", ""]*diff])
for d in zip(service_provider_data, client_info_data):
2015-06-08 16:30:47 +00:00
d[0].append('')
2015-06-08 15:55:05 +00:00
d[0].extend(d[1])
table_data.append(d[0])
self._story.append(
Table(table_data, style=table_style)
2015-06-06 09:31:18 +00:00
)
2015-06-08 15:55:05 +00:00
else:
self._build_service_provider_info()
self._build_client_info()
2015-06-10 14:55:53 +00:00
def _item_raw_data_and_subtotal(self):
2015-06-09 14:20:04 +00:00
item_data = []
item_subtotal = 0
for item in self._items:
if not isinstance(item, Item):
continue
item_data.append(
(
item.name,
Paragraph(item.description, self._defined_styles.get('TableParagraph')),
item.units,
item.unit_price,
item.amount
)
)
item_subtotal += item.amount
2015-06-06 09:31:18 +00:00
2015-06-10 14:55:53 +00:00
return item_data, item_subtotal
2015-06-09 14:20:04 +00:00
2015-06-10 14:55:53 +00:00
def _item_data_and_style(self):
# Items
item_data, item_subtotal = self._item_raw_data_and_subtotal()
style = []
2015-06-09 14:20:04 +00:00
2015-06-10 14:55:53 +00:00
if not item_data:
return item_data, style
2015-06-09 14:20:04 +00:00
2015-06-10 14:55:53 +00:00
self._story.append(
Paragraph('Detail', self._defined_styles.get('Heading1'))
)
2015-06-09 14:20:04 +00:00
2015-06-10 14:55:53 +00:00
item_data_title = ('Name', 'Description', 'Units', 'Unit Price', 'Amount')
item_data.insert(0, item_data_title) # Insert title
2015-06-09 14:20:04 +00:00
2015-06-10 14:55:53 +00:00
# Summary field
sum_start_y_index = len(item_data)
sum_end_x_index = -1 - 1
sum_start_x_index = len(item_data_title) - abs(sum_end_x_index)
# ##### Subtotal #####
rounditem_subtotal = self.getroundeddecimal(item_subtotal, self.precision)
2015-06-10 14:55:53 +00:00
item_data.append(
('Subtotal', '', '', '', rounditem_subtotal)
2015-06-10 14:55:53 +00:00
)
style.append(('SPAN', (0, sum_start_y_index), (sum_start_x_index, sum_start_y_index)))
style.append(('ALIGN', (0, sum_start_y_index), (sum_end_x_index, -1), 'RIGHT'))
# Tax total
if self._item_tax_rate is not None:
tax_total = item_subtotal * (Decimal(str(self._item_tax_rate)) / Decimal('100'))
roundtax_total = self.getroundeddecimal(tax_total, self.precision)
2015-06-10 14:55:53 +00:00
item_data.append(
('Vat/Tax ({0}%)'.format(self._item_tax_rate), '', '', '', roundtax_total)
2015-06-10 14:55:53 +00:00
)
2015-06-09 14:20:04 +00:00
sum_start_y_index += 1
style.append(('SPAN', (0, sum_start_y_index), (sum_start_x_index, sum_start_y_index)))
style.append(('ALIGN', (0, sum_start_y_index), (sum_end_x_index, -1), 'RIGHT'))
2015-06-10 14:55:53 +00:00
else:
tax_total = None
# Total
total = item_subtotal + (tax_total if tax_total else Decimal('0'))
roundtotal = self.getroundeddecimal(total, self.precision)
item_data.append(('Total', '', '', '', roundtotal))
2015-06-10 14:55:53 +00:00
sum_start_y_index += 1
style.append(('SPAN', (0, sum_start_y_index), (sum_start_x_index, sum_start_y_index)))
style.append(('ALIGN', (0, sum_start_y_index), (sum_end_x_index, -1), 'RIGHT'))
return item_data, style
2015-06-09 14:20:04 +00:00
def getroundeddecimal(self, nrtoround, precision):
d = Decimal(nrtoround)
aftercomma = Decimal(precision) # or anything that has the exponent depth you want
rvalue = Decimal(d.quantize(aftercomma, rounding='ROUND_HALF_UP'))
return rvalue
2015-06-10 14:55:53 +00:00
def _build_items(self):
item_data, style = self._item_data_and_style()
if item_data:
2015-06-09 13:25:42 +00:00
self._story.append(TableWithHeader(item_data, horizontal_align='LEFT', style=style))
2015-05-30 16:30:00 +00:00
2015-06-11 14:17:42 +00:00
def _transactions_data(self):
2015-06-06 09:31:18 +00:00
transaction_table_data = [
(
t.transaction_id,
2015-06-08 16:30:47 +00:00
Paragraph(t.gateway, self._defined_styles.get('TableParagraph')),
self._format_value(t.transaction_datetime),
2015-06-08 16:30:47 +00:00
t.amount,
2015-06-06 09:31:18 +00:00
) for t in self._transactions if isinstance(t, Transaction)
]
2015-06-06 09:17:57 +00:00
if transaction_table_data:
transaction_table_data.insert(0, ('Transaction id', 'Gateway', 'Transaction date', 'Amount'))
2015-06-11 14:17:42 +00:00
return transaction_table_data
def _build_transactions(self):
# Transaction
transaction_table_data = self._transactions_data()
if transaction_table_data:
self._story.append(Paragraph('Transaction', self._defined_styles.get('Heading1')))
2015-06-06 09:31:18 +00:00
self._story.append(TableWithHeader(transaction_table_data, horizontal_align='LEFT'))
def _build_bottom_tip(self):
2015-06-09 16:53:08 +00:00
if self._bottom_tip:
self._story.append(Spacer(5, 5))
self._story.append(
Paragraph(
self._bottom_tip,
ParagraphStyle(
'BottomTip',
parent=self._defined_styles.get('Normal'),
alignment=self._bottom_tip_align
)
)
)
2015-06-06 09:31:18 +00:00
def finish(self):
self._story = []
self._build_invoice_info()
self._build_service_provider_and_client_info()
self._build_items()
self._build_transactions()
self._build_bottom_tip()
2015-06-06 09:17:57 +00:00
2015-06-11 13:52:06 +00:00
kwargs = {}
if self.is_paid:
kwargs['onFirstPage'] = PaidStamp(7 * inch, 5.8 * inch)
self.build(self._story, **kwargs)