PyInvoice/pyinvoice/templates.py
2021-02-12 18:27:04 +00:00

329 lines
No EOL
12 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, Image
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')
precision = None
def __init__(self, invoice_path, pdf_info=None, precision='0.01'):
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.precision = precision
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
self._logo = None
self._build_kwargs = {}
@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 _invoice_info_data(self):
if isinstance(self.invoice_info, InvoiceInfo):
props = [('invoice_id', 'Invoice id'), ('invoice_datetime', 'Invoice date'),
('due_datetime', 'Invoice due date')]
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):
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)
return []
def _build_service_provider_info(self):
# Merchant
service_provider_info_data = self._service_provider_data()
if service_provider_info_data:
self._story.append(Paragraph('Service Provider', self._defined_styles.get('RightHeading1')))
self._story.append(SimpleTable(service_provider_info_data, horizontal_align='RIGHT'))
def _client_info_data(self):
if not isinstance(self.client_info, ClientInfo):
return []
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
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'))
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 _item_raw_data_and_subtotal(self):
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,
'{0:.2f}'.format(float(item.unit_price)),
'{0:.2f}'.format(item.amount)
)
)
item_subtotal += item.amount
return item_data, item_subtotal
def _item_data_and_style(self):
# Items
item_data, item_subtotal = self._item_raw_data_and_subtotal()
style = []
if not item_data:
return item_data, style
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)
# ##### Subtotal #####
rounditem_subtotal = self.getroundeddecimal(item_subtotal, self.precision)
item_data.append(
('Subtotal', '', '', '', '{0:.2f}'.format(rounditem_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'))
roundtax_total = self.getroundeddecimal(tax_total, self.precision)
item_data.append(
('Vat/Tax ({0}%)'.format(self._item_tax_rate), '', '', '', '{0:.2f}'.format(roundtax_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'))
roundtotal = self.getroundeddecimal(total, self.precision)
item_data.append(('Total', '', '', '', '{0:.2f}'.format(roundtotal)))
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
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
def _build_items(self):
item_data, style = self._item_data_and_style()
if item_data:
self._story.append(TableWithHeader(item_data, horizontal_align='LEFT', style=style))
def _transactions_data(self):
transaction_table_data = [
(
t.transaction_id,
Paragraph(t.gateway, self._defined_styles.get('TableParagraph')),
self._format_value(t.transaction_datetime),
'{0:.2f}'.format(t.amount),
) for t in self._transactions if isinstance(t, Transaction)
]
if transaction_table_data:
transaction_table_data.insert(0, ('Transaction id', 'Gateway', 'Transaction date', 'Amount'))
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')))
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 logo(self, logo):
if isinstance(logo, Image):
self._logo = logo
else:
self._logo = Image(logo)
def _build_logo(self):
if self._logo:
self._logo.hAlign = "LEFT"
self._logo.vAlign = "TOP"
self._story.append(self._logo)
def _build_paid_stamp(self):
if self.is_paid:
self._build_kwargs['onFirstPage'] = PaidStamp(3 * inch, -2 * inch)
def finish(self):
self._build_logo()
self._build_invoice_info()
self._build_service_provider_and_client_info()
self._build_items()
self._build_transactions()
self._build_bottom_tip()
self._build_paid_stamp()
self.build(self._story, **self._build_kwargs)