PyInvoice/pyinvoice/templates.py
2015-06-10 22:12:42 +08:00

277 lines
No EOL
10 KiB
Python

from __future__ import unicode_literals
from datetime import datetime, date
from decimal import Decimal
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Table, Spacer
from pyinvoice.components import SimpleTable, TableWithHeader, PaidStamp
from pyinvoice.models import PDFInfo, Item, Transaction, InvoiceInfo, ServiceProviderInfo, ClientInfo
class SimpleInvoice(SimpleDocTemplate):
default_pdf_info = PDFInfo(title='Invoice', author='CiCiApp.com', subject='Invoice')
def __init__(self, invoice_path, pdf_info=None):
if not pdf_info:
pdf_info = self.default_pdf_info
SimpleDocTemplate.__init__(
self,
invoice_path,
pagesize=letter,
rightMargin=inch,
leftMargin=inch,
topMargin=inch,
bottomMargin=inch,
**pdf_info.__dict__
)
self._defined_styles = getSampleStyleSheet()
self._defined_styles.add(
ParagraphStyle('RightHeading1', parent=self._defined_styles.get('Heading1'), alignment=TA_RIGHT)
)
self._defined_styles.add(
ParagraphStyle('TableParagraph', parent=self._defined_styles.get('Normal'), alignment=TA_CENTER)
)
self.invoice_info = None
self.service_provider_info = None
self.client_info = None
self.is_paid = False
self._items = []
self._item_tax_rate = None
self._transactions = []
self._story = []
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)
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)
def set_bottom_tip(self, text, align=TA_CENTER):
self._bottom_tip = text
self._bottom_tip_align = align
@staticmethod
def _format_value(value):
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 = []
for property_name, verbose_name in attribute_verbose_name_list:
attr = getattr(instance, property_name)
if attr:
attr = self._format_value(attr)
data.append(['{0}:'.format(verbose_name), attr])
return data
def _build_invoice_info(self):
if isinstance(self.invoice_info, InvoiceInfo):
self._story.append(
Paragraph('Invoice', self._defined_styles.get('RightHeading1'))
)
props = [('invoice_id', 'Invoice id'), ('invoice_datetime', 'Invoice date'),
('due_datetime', 'Invoice due date')]
self._story.append(
SimpleTable(self._attribute_to_table_data(self.invoice_info, props), horizontal_align='RIGHT')
)
def _service_provider_data(self):
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)
def _build_service_provider_info(self):
# Merchant
if isinstance(self.service_provider_info, ServiceProviderInfo):
self._story.append(
Paragraph('Merchant', self._defined_styles.get('RightHeading1'))
)
self._story.append(
SimpleTable(self._service_provider_data(), horizontal_align='RIGHT')
)
def _client_info_data(self):
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)
def _build_client_info(self):
# ClientInfo
if isinstance(self.client_info, ClientInfo):
self._story.append(
Paragraph('Client', self._defined_styles.get('Heading1'))
)
self._story.append(
SimpleTable(self._client_info_data(), horizontal_align='LEFT')
)
def _build_service_provider_and_client_info(self):
if isinstance(self.service_provider_info, ServiceProviderInfo) and isinstance(self.client_info, ClientInfo):
# Merge Table
table_data = [
[Paragraph('Service Provider', self._defined_styles.get('Heading1')), '', '', Paragraph('Client', self._defined_styles.get('Heading1')), '']
]
table_style = [
('SPAN', (0, 0), (1, 0)),
('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),
]
client_info_data = self._client_info_data()
service_provider_data = self._service_provider_data()
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):
d[0].append('')
d[0].extend(d[1])
table_data.append(d[0])
self._story.append(
Table(table_data, style=table_style)
)
else:
self._build_service_provider_info()
self._build_client_info()
def _build_items(self):
# Items
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
if item_data:
self._story.append(
Paragraph('Detail', self._defined_styles.get('Heading1'))
)
item_data_title = ('Name', 'Description', 'Units', 'Unit Price', 'Amount')
item_data.insert(0, item_data_title) # Insert title
# 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)
style = []
# ##### Subtotal #####
item_data.append(
('Subtotal', '', '', '', item_subtotal)
)
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'))
item_data.append(
('Vat/Tax ({0}%)'.format(self._item_tax_rate), '', '', '', tax_total)
)
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'))
else:
tax_total = None
# Total
total = item_subtotal + tax_total if tax_total else Decimal('0')
item_data.append(('Total', '', '', '', total))
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'))
self._story.append(TableWithHeader(item_data, horizontal_align='LEFT', style=style))
def _build_transactions(self):
# Transaction
transaction_table_data = [
(
t.transaction_id,
Paragraph(t.gateway, self._defined_styles.get('TableParagraph')),
self._format_value(t.transaction_datetime),
t.amount,
) for t in self._transactions if isinstance(t, Transaction)
]
if transaction_table_data:
self._story.append(
Paragraph('Transaction', self._defined_styles.get('Heading1'))
)
transaction_table_data.insert(0, ('Transaction id', 'Gateway', 'Transaction date', 'Amount'))
self._story.append(TableWithHeader(transaction_table_data, horizontal_align='LEFT'))
def _build_bottom_tip(self):
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
)
)
)
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()
self.build(self._story, onFirstPage=PaidStamp(7 * inch, 5.8 * inch) if self.is_paid else None)